- Gestaltung „Bauplan“: ruhigere Farben, Schriftskala, Abstände im 4-px-Raster, Kalenderwochen und Nummern in Mono-Ziffern, Glas beruhigt - Dashboard „Meine Aufgaben“: Kopf mit Kalenderwoche, Band „Diese Woche“ (Mo–Fr), je Projekt Meine Rolle / Team / Notizen; offene Termine blau hervorgehoben; Monatskalender unverändert - Neuer Bereich „Notizen“ in der Seitenleiste: Notizen aller eigenen Projekte und eigene Notizen ohne Projekt, später einem Projekt zuordenbar - Seitenleiste neu gegliedert (Projekte, Team, Notizen, Studenten, Einstellungen/Admin unten), Admin-Punkte in Personen/Vorgaben/System - Reiter im Projektkopf wie die Seitenleiste hervorgehoben - Behoben: „&amp;…“ in Kommentaren wuchs bei jedem Speichern; bestehende Kommentare werden beim Start repariert - Behoben: Lösch-Rückfrage bei Notizen unlesbar bzw. blieb stehen, Cursor im Notiztitel verdeckt, Dropdown im Dunkelmodus gekachelt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1115 lines
59 KiB
JavaScript
1115 lines
59 KiB
JavaScript
// -----------------------------
|
||
// Projektplan — Protokolle (Besprechungsprotokolle je Projekt)
|
||
// -----------------------------
|
||
// Bereich "Protokolle" des Projektbereichs (siehe js/plan/plan-sections.js)
|
||
// plus eine Kachel im Dashboard (#planDashProtokolle, springt per
|
||
// Klick über openProtokollePanel() in diesen Bereich — gleiches Muster wie
|
||
// #planFilesSummary/openFilesPanel() in plan-files.js).
|
||
//
|
||
// Protokolle hängen wie Dateien NUR an project_nr (nicht an week_label/
|
||
// project_id, das sich bei jedem automatischen Sitzungswechsel ändert), sind
|
||
// also bei einem Projekt ohne Projektnummer nicht möglich — der Bereich zeigt
|
||
// dann nur einen Hinweis, wie bei Dateien.
|
||
//
|
||
// Das "Fortschreiben" (offene Punkte wandern automatisch mit gleicher Nummer
|
||
// ins nächste Protokoll) rechnet ausschließlich der Server beim Anlegen
|
||
// (POST /project-protokolle, server.js) — hier wird nur angezeigt/bearbeitet.
|
||
// Ob ein Punkt "neu" ist (gelb markiert), steht serverseitig fest (Feld
|
||
// `neu`, unabhängig von `nummer`/`gruppe`, die frei editierbare Anzeige-Texte
|
||
// sind, siehe CLAUDE.md "### Protokolle").
|
||
//
|
||
// Die Detailansicht ist bewusst KEIN Modal (ein Protokoll mit vielen Punkten
|
||
// ist zu umfangreich dafür), sondern ersetzt inline die Listenansicht
|
||
// innerhalb desselben Bereichs-Containers. Um dabei den Eingabefokus beim
|
||
// Tippen nicht zu verlieren, wird bei einfachen Textfeldern NICHT bei jedem
|
||
// Tastendruck neu gerendert — nur strukturelle Änderungen (Zeile hinzufügen/
|
||
// entfernen, Gruppe ändern) lösen ein Neu-Rendern aus.
|
||
//
|
||
// Läuft wie alle plan-Module in einem IIFE; Zustand: window.PlanApp
|
||
// (plan-state.js).
|
||
|
||
(function(){
|
||
const app = window.PlanApp;
|
||
|
||
// protokolleCache: die zuletzt geladene Listenübersicht (Zusammenfassungen,
|
||
// keine vollen Punkte/Teilnehmer — die liefert erst das Öffnen eines
|
||
// einzelnen Protokolls). currentProtokoll: das volle, gerade in der
|
||
// Detailansicht bearbeitete Protokoll, oder null in der Listenansicht.
|
||
let protokolleCache = [];
|
||
let currentProtokoll = null;
|
||
|
||
// ---------- Laden ----------
|
||
|
||
// initPlanProtokoll: wird unbedingt aus renderPlanPage() aufgerufen (wie
|
||
// initPlanFiles()), nicht erst beim Öffnen des Bereichs — sonst zeigte die
|
||
// Dashboard-Kompaktkarte veraltete Zahlen, bis man den Bereich einmal
|
||
// betreten hat.
|
||
window.initPlanProtokoll = function(){
|
||
const hasNr = !!(app.project && app.project.nr);
|
||
const hint = document.getElementById('protokolleNoProjectHint');
|
||
const container = document.getElementById('protokolleContainer');
|
||
if(hint) hint.style.display = hasNr ? 'none' : '';
|
||
if(container) container.style.display = hasNr ? '' : 'none';
|
||
currentProtokoll = null;
|
||
if(!hasNr){
|
||
protokolleCache = [];
|
||
renderProtokolleSummary();
|
||
return;
|
||
}
|
||
ladeProtokollListe();
|
||
};
|
||
|
||
// ladeProtokollListe: GET /project-protokolle?projectNr= — füllt
|
||
// protokolleCache und rendert Listenansicht + Dashboard-Kompaktkarte neu.
|
||
function ladeProtokollListe(){
|
||
const container = document.getElementById('protokolleContainer');
|
||
if(container && !currentProtokoll) container.innerHTML = '<span class="hint">Lade Protokolle…</span>';
|
||
fetch(baseUrl() + '/project-protokolle?projectNr=' + encodeURIComponent(app.project.nr))
|
||
.then(r => { if(!r.ok) throw new Error('Server HTTP ' + r.status); return r.json(); })
|
||
.then(data => {
|
||
protokolleCache = Array.isArray(data.protokolle) ? data.protokolle : [];
|
||
window.renderPlanProtokolle();
|
||
renderProtokolleSummary();
|
||
})
|
||
.catch(err => {
|
||
if(container) container.innerHTML = '<span class="hint">Protokolle konnten nicht geladen werden.</span>';
|
||
console.error('Protokollliste laden fehlgeschlagen:', err);
|
||
});
|
||
}
|
||
|
||
// renderProtokolleSummary: füllt die Dashboard-Kachel "Protokolle"
|
||
// (#planDashProtokolle, Eriks Wunsch — vorher nur eine Zeile in
|
||
// "Unterlagen"). Oben das letzte Protokoll, darunter dessen OFFENE Punkte
|
||
// (die Frage nach einer Besprechung ist "was ist noch offen?"), unten die
|
||
// früheren Protokolle als Kurzliste. Die Punkte kommen aus einem zweiten
|
||
// Abruf (GET /project-protokolle/:id) — die Liste liefert nur Zahlen.
|
||
const DASH_PROTOKOLL_PUNKTE = 5;
|
||
const DASH_PROTOKOLL_FRUEHERE = 3;
|
||
let dashProtokollAbruf = 0;
|
||
|
||
function datumDe(iso){
|
||
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(iso || ''));
|
||
return m ? `${m[3]}.${m[2]}.${m[1]}` : String(iso || '');
|
||
}
|
||
|
||
function renderProtokolleSummary(){
|
||
const el = document.getElementById('planDashProtokolle');
|
||
const panel = document.getElementById('planDashProtokollePanel');
|
||
if(!el) return;
|
||
// Ohne Projektnummer (Planung-Terminplan) gibt es keine Protokolle —
|
||
// die Kachel verschwindet wie der Bereich selbst.
|
||
if(panel) panel.hidden = !(app.project && app.project.nr);
|
||
if(!app.project || !app.project.nr){ el.innerHTML = ''; return; }
|
||
if(protokolleCache.length === 0){
|
||
el.innerHTML = `<div class="plan-dash-leer">Noch keine Protokolle.
|
||
<button type="button" class="plan-panel-link" onclick="dashNeuesProtokoll()">+ Neues Protokoll</button></div>`;
|
||
return;
|
||
}
|
||
const letztes = protokolleCache[0]; // Server liefert nach nummer DESC
|
||
const frueher = protokolleCache.slice(1, 1 + DASH_PROTOKOLL_FRUEHERE);
|
||
const zahlen = [
|
||
letztes.offenCount ? `<span class="plan-mini-chip warn">${letztes.offenCount} offen</span>` : '<span class="plan-mini-chip">nichts offen</span>',
|
||
letztes.erledigtCount ? `<span class="plan-mini-chip">${letztes.erledigtCount} erledigt</span>` : '',
|
||
letztes.infoCount ? `<span class="plan-mini-chip">${letztes.infoCount} Info</span>` : ''
|
||
].join('');
|
||
el.innerHTML = `
|
||
<button type="button" class="plan-prot-kopf" onclick="dashOeffneProtokoll('${esc(letztes.id)}')">
|
||
<span class="plan-prot-nr">Nr. ${esc(letztes.nummer)}</span>
|
||
<span class="plan-prot-titel"><b>${esc(letztes.betreff || 'ohne Betreff')}</b>
|
||
<span>${esc(datumDe(letztes.datum))}${letztes.ort ? ' · ' + esc(letztes.ort) : ''}${letztes.teilnehmerCount ? ' · ' + letztes.teilnehmerCount + ' Teilnehmer' : ''}</span></span>
|
||
</button>
|
||
<div class="plan-mini-chip-row">${zahlen}</div>
|
||
<div class="plan-prot-punkte" id="planDashProtokollPunkte">${letztes.offenCount ? '<span class="hint">Lade offene Punkte…</span>' : ''}</div>
|
||
${frueher.length ? `<div class="plan-termin-gruppe">Frühere Protokolle</div>
|
||
${frueher.map(pr => `<button type="button" class="plan-prot-frueher" onclick="dashOeffneProtokoll('${esc(pr.id)}')">
|
||
<span>Nr. ${esc(pr.nummer)} · ${esc(datumDe(pr.datum))}</span>
|
||
<span class="plan-prot-frueher-titel">${esc(pr.betreff || 'ohne Betreff')}</span>
|
||
<span class="plan-prot-frueher-zahl">${pr.offenCount ? pr.offenCount + ' offen' : ''}</span>
|
||
</button>`).join('')}` : ''}`;
|
||
if(letztes.offenCount) ladeOffenePunkte(letztes.id);
|
||
}
|
||
|
||
function ladeOffenePunkte(id){
|
||
const nr = ++dashProtokollAbruf;
|
||
fetch(baseUrl() + '/project-protokolle/' + encodeURIComponent(id))
|
||
.then(r => { if(!r.ok) throw new Error('Server HTTP ' + r.status); return r.json(); })
|
||
.then(d => {
|
||
if(nr !== dashProtokollAbruf) return; // inzwischen anderes Projekt
|
||
const ziel = document.getElementById('planDashProtokollPunkte');
|
||
if(!ziel || !d.protokoll) return;
|
||
const punkte = d.protokoll.punkte || [];
|
||
const nummern = window.berechneProtokollNummern(punkte);
|
||
const heute = typeof heuteIso === 'function' ? heuteIso() : new Date().toISOString().slice(0, 10);
|
||
const offen = punkte.map((p, i) => Object.assign({}, p, { anzeigeNr: nummern[i] }))
|
||
.filter(p => p.status === 'offen')
|
||
// Überfälliges zuerst, dann nach Termin, ohne Termin zuletzt
|
||
.sort((a, b) => (a.termin || '9999').localeCompare(b.termin || '9999'));
|
||
const zeigen = offen.slice(0, DASH_PROTOKOLL_PUNKTE);
|
||
const rest = offen.length - zeigen.length;
|
||
ziel.innerHTML = zeigen.map(p => {
|
||
const ueber = p.termin && p.termin < heute;
|
||
const titel = p.thema || statusPlainText(p.text).slice(0, 80) || 'ohne Thema';
|
||
return `<div class="plan-termin ist-klickbar" onclick="dashOeffneProtokoll('${esc(d.protokoll.id)}')">
|
||
<span class="plan-termin-art">${esc(p.anzeigeNr)}</span>
|
||
<span class="plan-termin-text"><b>${esc(titel)}</b><span>${p.gruppe ? esc(p.gruppe) + ' · ' : ''}${p.zustaendig ? esc(p.zustaendig) : 'ohne Zuständigen'}</span></span>
|
||
<span class="plan-termin-datum${ueber ? ' ist-rot' : ''}">${p.termin ? esc(datumDe(p.termin)) + (typeof relativerTag === 'function' ? '<em>' + esc(relativerTag(p.termin)) + '</em>' : '') : '—'}</span>
|
||
</div>`;
|
||
}).join('') + (rest > 0 ? `<button type="button" class="plan-panel-link plan-prot-mehr" onclick="dashOeffneProtokoll('${esc(d.protokoll.id)}')">… ${rest} weitere offene Punkte</button>` : '');
|
||
})
|
||
.catch(err => {
|
||
if(nr !== dashProtokollAbruf) return;
|
||
const ziel = document.getElementById('planDashProtokollPunkte');
|
||
if(ziel) ziel.innerHTML = '<span class="hint">Offene Punkte konnten nicht geladen werden.</span>';
|
||
console.error('Offene Protokollpunkte laden fehlgeschlagen:', err);
|
||
});
|
||
}
|
||
|
||
// dashOeffneProtokoll: aus der Kachel direkt in DIESES Protokoll.
|
||
window.dashOeffneProtokoll = function(id){
|
||
window.openProtokollePanel();
|
||
window.oeffneProtokoll(id);
|
||
};
|
||
window.dashNeuesProtokoll = function(){
|
||
window.openProtokollePanel();
|
||
window.neuesProtokoll();
|
||
};
|
||
|
||
// openProtokollePanel: springt in den Bereich "Protokolle" — Gegenstück zu
|
||
// openFilesPanel()/openResourceModal().
|
||
window.openProtokollePanel = function(){
|
||
if(typeof showPlanSection === 'function') showPlanSection('protokolle');
|
||
};
|
||
|
||
// ---------- Nummer-/Gruppen-Hilfsfunktionen ----------
|
||
|
||
// berechneProtokollNummern: errechnet die Anzeige-Nummer JEDES Punkts rein
|
||
// aus seiner Position — "1.1"/"1.2"/"1.3" für Punkte innerhalb einer Gruppe
|
||
// (die erste Ziffer ist die Gruppennummer, die zweite die laufende Position
|
||
// INNERHALB dieser Gruppe), schlichte "1"/"2"/"3" für Punkte ohne Gruppe.
|
||
// Gruppennummer UND die Nummer eines Punkts ohne Gruppe kommen aus EINEM
|
||
// gemeinsamen, fortlaufenden Zähler (nicht zwei getrennten) — sonst gäbe es
|
||
// z. B. sowohl einen Punkt "1" ohne Gruppe als auch eine Gruppe mit der
|
||
// Nummer "1" (Punkte "1.1", "1.2" …), zwei verschiedene Dinge mit
|
||
// derselben führenden Ziffer. Jede oberste Ebene (ob Einzelpunkt oder
|
||
// Gruppe) bekommt dadurch eine über das ganze Protokoll hinweg einmalige
|
||
// Zahl. Nie an einen gespeicherten Wert gebunden — ändert sich die
|
||
// Reihenfolge oder die Gruppe eines Punkts, verschiebt sich die Nummer
|
||
// automatisch mit, exakt wie bei einem klassischen Baubesprechungsprotokoll
|
||
// mit durchnummerierten Abschnitten. Global verfügbar, weil sowohl die
|
||
// Detailansicht hier als auch exportProtokollPDF() (js/pdf.js) dieselbe
|
||
// Nummerierung zeigen müssen — dasselbe Prinzip wie classifyProjectChange()
|
||
// aus wochenbericht.js, das pdf.js ebenfalls direkt aufruft.
|
||
// Parameter: punkte (Array in Anzeige-/array_order-Reihenfolge). Rückgabe:
|
||
// Array von Strings, indexgleich zu punkte.
|
||
window.berechneProtokollNummern = function(punkte){
|
||
const gruppenNr = new Map();
|
||
const positionInGruppe = new Map();
|
||
let naechsteTopLevelNr = 1;
|
||
return (punkte || []).map(p => {
|
||
const g = String(p.gruppe || '').trim();
|
||
if(!g) return String(naechsteTopLevelNr++);
|
||
if(!gruppenNr.has(g)){
|
||
gruppenNr.set(g, naechsteTopLevelNr++);
|
||
positionInGruppe.set(g, 0);
|
||
}
|
||
const pos = positionInGruppe.get(g) + 1;
|
||
positionInGruppe.set(g, pos);
|
||
return gruppenNr.get(g) + '.' + pos;
|
||
});
|
||
};
|
||
|
||
// gruppenReihenfolge: liefert die vorkommenden Gruppennamen in der
|
||
// Reihenfolge ihres ersten Auftretens unter den Punkten (array_order).
|
||
// Jeder Punkt gehört jetzt zu einer Gruppe (siehe fuegeNeueGruppeHinzu()/
|
||
// "+ Neue Gruppe" unten) — eine frühere Sonderrolle "Ohne Gruppe" (leerer
|
||
// String, immer zuerst) gibt es nicht mehr; ein einzelner Punkt mit leerer
|
||
// Gruppe (nur noch aus Altbeständen von vor dieser Umstellung möglich)
|
||
// bekommt ganz normal eine eigene, leer betitelte Gruppenzeile wie jede
|
||
// andere — kein Sonderfall im Code nötig.
|
||
function gruppenReihenfolge(){
|
||
const namen = [];
|
||
(currentProtokoll.punkte || []).forEach(p => {
|
||
const g = String(p.gruppe || '').trim();
|
||
if(!namen.includes(g)) namen.push(g);
|
||
});
|
||
return namen;
|
||
}
|
||
|
||
// ---------- Nachträgliches Umordnen ----------
|
||
//
|
||
// Die Reihenfolge in `currentProtokoll.punkte` (= array_order) bestimmt
|
||
// sowohl die Anzeige-Reihenfolge als auch — über berechneProtokollNummern()
|
||
// — die Nummern. Zwei getrennte, aufeinander abgestimmte Stellschrauben:
|
||
// ein Punkt lässt sich nur INNERHALB seiner eigenen Gruppe verschieben (die
|
||
// Zeile steht dort ohnehin nur im passenden Abschnitt), eine ganze Gruppe
|
||
// lässt sich als Block gegenüber den anderen Gruppen verschieben — das
|
||
// ändert, welche Gruppe die Nummer "1" bekommt.
|
||
|
||
// verschiebePunkt: vertauscht einen Punkt mit seinem Vorgänger/Nachfolger
|
||
// INNERHALB derselben Gruppe. Parameter: idx (Index in
|
||
// currentProtokoll.punkte), richtung (-1 = nach oben, +1 = nach unten).
|
||
// Keine Rückgabe; rendert nur den Punkte-Block neu.
|
||
window.verschiebePunkt = function(idx, richtung){
|
||
if(!currentProtokoll || app.protokolleGesperrt()) return;
|
||
const punkte = currentProtokoll.punkte;
|
||
const gruppe = String(punkte[idx].gruppe || '').trim();
|
||
const gleicheGruppeIdx = punkte.map((p, i) => i).filter(i => String(punkte[i].gruppe || '').trim() === gruppe);
|
||
const posInGruppe = gleicheGruppeIdx.indexOf(idx);
|
||
const zielPos = posInGruppe + richtung;
|
||
if(zielPos < 0 || zielPos >= gleicheGruppeIdx.length) return;
|
||
const zielIdx = gleicheGruppeIdx[zielPos];
|
||
const tmp = punkte[idx];
|
||
punkte[idx] = punkte[zielIdx];
|
||
punkte[zielIdx] = tmp;
|
||
const block = document.getElementById('protokollPunkteBlock');
|
||
if(block) block.innerHTML = renderPunkteBlockHtml();
|
||
};
|
||
|
||
// verschiebeGruppe: verschiebt eine ganze Gruppe (alle ihre Punkte als
|
||
// Block, ihre eigene Reihenfolge bleibt untereinander erhalten) gegenüber
|
||
// den anderen Gruppen — ändert damit auch die Gruppennummer. Baut die
|
||
// Punkteliste dafür komplett in der neuen Gruppenreihenfolge neu auf.
|
||
// Parameter: gruppe (Name), richtung (-1 = nach vorn, +1 = nach hinten).
|
||
// Keine Rückgabe.
|
||
window.verschiebeGruppe = function(gruppe, richtung){
|
||
if(!currentProtokoll || app.protokolleGesperrt()) return;
|
||
const punkte = currentProtokoll.punkte;
|
||
const namen = [];
|
||
punkte.forEach(p => {
|
||
const g = String(p.gruppe || '').trim();
|
||
if(!namen.includes(g)) namen.push(g);
|
||
});
|
||
const pos = namen.indexOf(String(gruppe || '').trim());
|
||
const zielPos = pos + richtung;
|
||
if(pos === -1 || zielPos < 0 || zielPos >= namen.length) return;
|
||
const tmpName = namen[pos];
|
||
namen[pos] = namen[zielPos];
|
||
namen[zielPos] = tmpName;
|
||
const neu = [];
|
||
namen.forEach(g => {
|
||
punkte.filter(p => String(p.gruppe || '').trim() === g).forEach(p => neu.push(p));
|
||
});
|
||
currentProtokoll.punkte = neu;
|
||
const block = document.getElementById('protokollPunkteBlock');
|
||
if(block) block.innerHTML = renderPunkteBlockHtml();
|
||
};
|
||
|
||
// ---------- Listenansicht ----------
|
||
|
||
function todoStatusZeile(offen, erledigt){
|
||
if(offen === 0 && erledigt === 0) return 'keine Punkte';
|
||
if(offen === 0) return 'alle erledigt';
|
||
return offen + ' offen · ' + erledigt + ' erledigt';
|
||
}
|
||
|
||
function renderProtokolleListe(){
|
||
const container = document.getElementById('protokolleContainer');
|
||
if(!container) return;
|
||
const locked = app.protokolleGesperrt();
|
||
const anlegeBtns = locked ? '' : `<button onclick="neuesProtokoll()">+ Neues Protokoll</button>
|
||
<button class="secondary" onclick="oeffneProtokollImportDialog()">Protokoll importieren</button>`;
|
||
if(protokolleCache.length === 0){
|
||
container.innerHTML = `<div class="plan-panel">
|
||
<div class="protokoll-list-head"><h3 class="plan-panel-title" style="margin:0">Protokolle</h3><span class="toolbar">${anlegeBtns}</span></div>
|
||
<span class="hint">${locked ? 'Für diese Sitzung sind keine Protokolle hinterlegt.' : 'Noch keine Protokolle. Über „+ Neues Protokoll" anlegen oder ein bestehendes importieren.'}</span>
|
||
</div>`;
|
||
return;
|
||
}
|
||
const zeilen = protokolleCache.map(p => `
|
||
<div class="protokoll-row" onclick="oeffneProtokoll('${esc(p.id)}')">
|
||
<span class="protokoll-nr">Nr. ${esc(p.nummer)}</span>
|
||
<span class="protokoll-betreff">${esc(p.betreff || 'ohne Betreff')}<span class="protokoll-datum">${esc(formatDateDE(parseDateISO(p.datum)) || '')}${p.ort ? ' · ' + esc(p.ort) : ''}</span></span>
|
||
<span class="protokoll-verfasser">${esc(p.verfasserName || '')}</span>
|
||
<span class="protokoll-punkte-status${p.offenCount === 0 ? ' ist-leer' : ''}"><span class="protokoll-status-dot"></span>${esc(todoStatusZeile(p.offenCount, p.erledigtCount))}</span>
|
||
<span class="protokoll-teilnehmer-count">${p.teilnehmerCount} Teilnehmer</span>
|
||
</div>`).join('');
|
||
container.innerHTML = `<div class="plan-panel">
|
||
<div class="protokoll-list-head"><h3 class="plan-panel-title" style="margin:0">Protokolle</h3><span class="toolbar">${anlegeBtns}</span></div>
|
||
${zeilen}
|
||
</div>`;
|
||
}
|
||
|
||
// ---------- Anlegen ----------
|
||
|
||
// neuesProtokoll: legt ein neues Protokoll an. Vorbelegung der Teilnehmer
|
||
// (rein clientseitige Komfort-Logik vor dem POST, siehe CLAUDE.md): Namen
|
||
// aus dem LETZTEN vorherigen Protokoll übernehmen (Anwesenheit auf
|
||
// "anwesend" zurückgesetzt), sonst Fallback auf projectAssignees(), sonst
|
||
// leer. Das eigentliche Fortschreiben der Punkte übernimmt der Server.
|
||
window.neuesProtokoll = async function(){
|
||
if(app.protokolleGesperrt()) return;
|
||
let teilnehmer = [];
|
||
try {
|
||
if(protokolleCache.length > 0){
|
||
const r = await fetch(baseUrl() + '/project-protokolle/' + encodeURIComponent(protokolleCache[0].id));
|
||
const d = await r.json();
|
||
if(r.ok && d.ok && d.protokoll){
|
||
teilnehmer = d.protokoll.teilnehmer.map(t => ({ name: t.name, firma: t.firma, anwesend: true }));
|
||
}
|
||
}
|
||
if(teilnehmer.length === 0){
|
||
const zugeordnet = projectAssignees(app.project, app.week);
|
||
const gesehen = new Set();
|
||
zugeordnet.forEach(a => {
|
||
if(gesehen.has(a.person.id)) return;
|
||
gesehen.add(a.person.id);
|
||
teilnehmer.push({ name: a.person.fullName || a.person.name, firma: '', anwesend: true });
|
||
});
|
||
}
|
||
} catch(err){
|
||
console.error('Teilnehmer-Vorbelegung fehlgeschlagen:', err);
|
||
}
|
||
|
||
try {
|
||
const url = baseUrl() + '/project-protokolle'
|
||
+ '?projectNr=' + encodeURIComponent(app.project.nr)
|
||
+ '&projectName=' + encodeURIComponent(app.project.name || '')
|
||
+ '&weekLabel=' + encodeURIComponent(app.week ? app.week.label : '');
|
||
const r = await fetch(url, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
||
body: JSON.stringify({ datum: heuteIso(), betreff: '', ort: '', teilnehmer })
|
||
});
|
||
const d = await r.json();
|
||
if(!r.ok || !d.ok){ alert('Protokoll konnte nicht angelegt werden: ' + (d && d.error ? d.error : 'unbekannter Fehler')); return; }
|
||
currentProtokoll = d.protokoll;
|
||
renderProtokollDetail();
|
||
ladeProtokollListe();
|
||
} catch(err){
|
||
console.error('Protokoll anlegen fehlgeschlagen:', err);
|
||
alert('Protokoll konnte nicht angelegt werden: ' + (err.message || err));
|
||
}
|
||
};
|
||
|
||
// ---------- Import (externes Protokoll hochladen) ----------
|
||
|
||
// oeffneProtokollImportDialog: zeigt den Import-Dialog, Datum vorbelegt
|
||
// mit heute (dieselbe Vorbelegung wie neuesProtokoll()). Das Erkennungs-
|
||
// Auswahlfeld wird hier aus GET /my-ai-settings aufgebaut statt fest im
|
||
// Markup zu stehen — der Hinweis "(kein Schlüssel hinterlegt)" bezieht
|
||
// sich auf den EIGENEN Schlüssel des angemeldeten Kontos (siehe
|
||
// Einstellungen → Mein Konto → KI-Zugangsdaten), nicht mehr auf einen
|
||
// firmenweiten Stand, und kann sich seit dem letzten Öffnen geändert
|
||
// haben.
|
||
window.oeffneProtokollImportDialog = function(){
|
||
if(app.protokolleGesperrt()) return;
|
||
document.getElementById('protokollImportFile').value = '';
|
||
document.getElementById('protokollImportBetreff').value = '';
|
||
document.getElementById('protokollImportOrt').value = '';
|
||
document.getElementById('protokollImportDatum').value = heuteIso();
|
||
document.getElementById('protokollImportDatumDisplay').value = formatDateDE(parseDateISO(heuteIso())) || '';
|
||
const erkennungSel = document.getElementById('protokollImportErkennung');
|
||
if(erkennungSel){
|
||
erkennungSel.innerHTML = `<option value="chatgpt">ChatGPT</option><option value="claude">Claude</option>`;
|
||
fetch(baseUrl() + '/my-ai-settings')
|
||
.then(r => r.json())
|
||
.then(d => {
|
||
if(!d.ok) return;
|
||
erkennungSel.innerHTML = `
|
||
<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(err => console.error('KI-Einstellungen laden fehlgeschlagen:', err));
|
||
}
|
||
const errEl = document.getElementById('protokollImportError');
|
||
if(errEl){ errEl.textContent = ''; errEl.hidden = true; }
|
||
setzeProtokollImportLadezustand(false);
|
||
document.getElementById('protokollImportModal').style.display = 'flex';
|
||
};
|
||
|
||
window.schliesseProtokollImportDialog = function(){
|
||
document.getElementById('protokollImportModal').style.display = 'none';
|
||
};
|
||
|
||
// setzeProtokollImportLadezustand: blendet den Ladehinweis ein/aus und
|
||
// sperrt alle Felder + Knöpfe des Dialogs währenddessen — ohne das gab es
|
||
// während eines länger laufenden KI-Aufrufs (bis zu KI_TIMEOUT_MS_LOKAL =
|
||
// 30 Minuten bei einem eigenen Server, server.js) keinerlei Rückmeldung,
|
||
// der Dialog wirkte hängengeblieben, und ein zweiter Klick auf
|
||
// "Importieren" hätte denselben Import doppelt losgeschickt.
|
||
function setzeProtokollImportLadezustand(aktiv){
|
||
['protokollImportFile', 'protokollImportErkennung', 'protokollImportDatumDisplay',
|
||
'protokollImportBetreff', 'protokollImportOrt'].forEach(id => {
|
||
const el = document.getElementById(id);
|
||
if(el) el.disabled = aktiv;
|
||
});
|
||
const modal = document.getElementById('protokollImportModal');
|
||
if(modal) modal.querySelectorAll('.toolbar button').forEach(btn => { btn.disabled = aktiv; });
|
||
const ladeEl = document.getElementById('protokollImportLoading');
|
||
if(ladeEl) ladeEl.hidden = !aktiv;
|
||
}
|
||
|
||
// importiereProtokoll: liest die gewählte Datei + Kopfdaten, schickt die
|
||
// Datei als rohen Binär-Body an POST /project-protokolle/import (gleiches
|
||
// Vorgehen wie submitFileUpload() in js/plan/plan-files.js, andere
|
||
// Zielroute). Bei Erfolg landet der Nutzer direkt in der ganz normalen,
|
||
// voll editierbaren Detailansicht — die erkannten Punkte sind darin
|
||
// bereits sichtbar (Hinweisbanner in renderProtokollDetail(), siehe dort)
|
||
// und lassen sich dort wie jeder andere Punkt prüfen/korrigieren, bevor
|
||
// gespeichert wird.
|
||
window.importiereProtokoll = function(){
|
||
const errEl = document.getElementById('protokollImportError');
|
||
const zeigeFehler = msg => { if(errEl){ errEl.textContent = msg; errEl.hidden = false; } };
|
||
if(errEl){ errEl.textContent = ''; errEl.hidden = true; }
|
||
|
||
const fileInput = document.getElementById('protokollImportFile');
|
||
const file = fileInput && fileInput.files && fileInput.files[0];
|
||
if(!file) return zeigeFehler('Bitte eine Datei auswählen.');
|
||
const datum = document.getElementById('protokollImportDatum').value;
|
||
if(!datum) return zeigeFehler('Bitte ein Datum wählen.');
|
||
|
||
const betreff = document.getElementById('protokollImportBetreff').value;
|
||
const ort = document.getElementById('protokollImportOrt').value;
|
||
const erkennungSel = document.getElementById('protokollImportErkennung');
|
||
const erkennung = erkennungSel ? erkennungSel.value : '';
|
||
const url = baseUrl() + '/project-protokolle/import'
|
||
+ '?projectNr=' + encodeURIComponent(app.project.nr)
|
||
+ '&projectName=' + encodeURIComponent(app.project.name || '')
|
||
+ '&weekLabel=' + encodeURIComponent(app.week ? app.week.label : '')
|
||
+ '&datum=' + encodeURIComponent(datum)
|
||
+ '&betreff=' + encodeURIComponent(betreff)
|
||
+ '&ort=' + encodeURIComponent(ort)
|
||
+ '&erkennung=' + encodeURIComponent(erkennung)
|
||
+ '&filename=' + encodeURIComponent(file.name);
|
||
|
||
setzeProtokollImportLadezustand(true);
|
||
fetch(url, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': file.type || 'application/octet-stream' },
|
||
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) || 'Import fehlgeschlagen.');
|
||
schliesseProtokollImportDialog();
|
||
currentProtokoll = d.protokoll;
|
||
// Angeforderte KI nicht verfügbar gewesen -> automatisch auf
|
||
// Textmuster zurückgefallen (server.js, POST .../import) — rein
|
||
// clientseitiges, EINMALIGES Merkmal (nie vom Server geliefert,
|
||
// nie gespeichert), das renderProtokollDetail() in einen deutlich
|
||
// sichtbaren, bestehen bleibenden Hinweis übersetzt (nicht nur
|
||
// einen nach 2,5s verschwindenden Toast, der beim gleichzeitigen
|
||
// Wechsel in die Detailansicht leicht übersehen wird — genau das
|
||
// war Eriks Rückmeldung, nachdem die Erkennung bei ihm an LM
|
||
// Studio scheiterte und er davon nichts in der Oberfläche sah).
|
||
if(erkennung && d.erkennungsart === 'textmuster' && d.erkennungshinweis){
|
||
currentProtokoll._kiFallbackHinweis = d.erkennungshinweis;
|
||
}
|
||
renderProtokollDetail();
|
||
ladeProtokollListe();
|
||
})
|
||
.catch(err => {
|
||
console.error('Protokoll-Import fehlgeschlagen:', err);
|
||
zeigeFehler(err.message || String(err));
|
||
})
|
||
.finally(() => setzeProtokollImportLadezustand(false));
|
||
};
|
||
|
||
// oeffneProtokoll: lädt das volle Protokoll (Teilnehmer + Punkte) und
|
||
// wechselt in die Detailansicht.
|
||
window.oeffneProtokoll = function(id){
|
||
const container = document.getElementById('protokolleContainer');
|
||
if(container) container.innerHTML = '<span class="hint">Lade Protokoll…</span>';
|
||
fetch(baseUrl() + '/project-protokolle/' + encodeURIComponent(id))
|
||
.then(r => { if(!r.ok) throw new Error('Server HTTP ' + r.status); return r.json(); })
|
||
.then(d => {
|
||
if(!d.ok || !d.protokoll) throw new Error(d.error || 'nicht gefunden');
|
||
currentProtokoll = d.protokoll;
|
||
renderProtokollDetail();
|
||
})
|
||
.catch(err => {
|
||
console.error('Protokoll laden fehlgeschlagen:', err);
|
||
if(container) container.innerHTML = '<span class="hint">Protokoll konnte nicht geladen werden.</span>';
|
||
});
|
||
};
|
||
|
||
// schliesseProtokollDetail: zurück zur Listenansicht.
|
||
window.schliesseProtokollDetail = function(){
|
||
currentProtokoll = null;
|
||
renderProtokolleListe();
|
||
};
|
||
|
||
// ---------- Detailansicht ----------
|
||
|
||
function renderProtokollDetail(){
|
||
const container = document.getElementById('protokolleContainer');
|
||
if(!container || !currentProtokoll) return;
|
||
const p = currentProtokoll;
|
||
const locked = app.protokolleGesperrt();
|
||
|
||
const datumDisplay = formatDateDE(parseDateISO(p.datum)) || '';
|
||
const quelleHinweis = p.quelleDateiname
|
||
? ` · importiert aus <a href="${baseUrl()}/project-protokolle/${esc(p.id)}/quelldatei" target="_blank" rel="noopener">${esc(p.quelleDateiname)}</a>`
|
||
: '';
|
||
// Hinweisbanner: nur bei einem importierten Protokoll (quelleDateiname
|
||
// gesetzt) — die automatisch erkannten Punkte sind ein Entwurf, siehe
|
||
// erkennePunkteAusText() (server.js) und CLAUDE.md "### Protokolle".
|
||
// Kein eigener Zustand: leitet sich allein aus dem geladenen Protokoll
|
||
// ab, verschwindet also nie von selbst, sondern nur wenn man das
|
||
// Protokoll erneut öffnet — bewusst so, die Herkunft bleibt sichtbar.
|
||
const importBanner = p.quelleDateiname
|
||
? `<div class="protokoll-import-banner">Punkte automatisch aus „${esc(p.quelleDateiname)}“ erkannt — bitte vor dem Speichern prüfen und bei Bedarf korrigieren.</div>`
|
||
: '';
|
||
// Rein clientseitiges Merkmal (siehe importiereProtokoll() oben) — nur
|
||
// direkt nach GENAU dem Import gesetzt, der die KI angefordert, aber
|
||
// nicht erreicht hat. Bewusst deutlich wärmer als importBanner oben
|
||
// (amber statt dem neutralen Gelb-Ton), damit ein echter Fehlschlag
|
||
// nicht im selben Ton wie die übliche "bitte prüfen"-Erinnerung
|
||
// untergeht, und bewusst kein Toast (verschwindet von selbst) — genau
|
||
// das war Eriks Rückmeldung, nachdem er von einem KI-Fehlschlag nichts
|
||
// in der Oberfläche sah.
|
||
const kiFallbackBanner = p._kiFallbackHinweis
|
||
? `<div class="protokoll-ki-fallback-banner">Die KI-Erkennung ist fehlgeschlagen (${esc(p._kiFallbackHinweis)}) — die Punkte wurden stattdessen per Textmuster erkannt.</div>`
|
||
: '';
|
||
const kopf = `
|
||
${kiFallbackBanner}
|
||
${importBanner}
|
||
<div class="protokoll-detail-head">
|
||
<div>
|
||
<h3 style="margin:0 0 4px">${esc(p.betreff || 'ohne Betreff')}</h3>
|
||
<div class="protokoll-detail-meta">Protokoll Nr. ${esc(p.nummer)} · verfasst von ${esc(p.verfasserName || '—')}${quelleHinweis}</div>
|
||
</div>
|
||
<div class="toolbar">
|
||
${locked ? '' : `<button class="secondary" onclick="exportProtokollPDF('${esc(p.id)}')">Als PDF exportieren</button>`}
|
||
${locked ? '' : `<button class="secondary" onclick="loescheProtokoll()">Löschen</button>`}
|
||
${locked ? '' : `<button onclick="speichereProtokoll()">Speichern</button>`}
|
||
</div>
|
||
</div>
|
||
<div class="protokoll-field-grid">
|
||
<div class="protokoll-field">
|
||
<label>Datum</label>
|
||
<input type="text" id="protokollDatumDisplay" class="range-picker-display" value="${esc(datumDisplay)}" placeholder="Datum wählen…" readonly
|
||
onclick="openDatePicker({ anchor: this, inputId: 'protokollDatum', displayId: 'protokollDatumDisplay', onApply: syncProtokollDatum })">
|
||
<input type="hidden" id="protokollDatum" value="${esc(p.datum)}">
|
||
</div>
|
||
<div class="protokoll-field">
|
||
<label>Betreff</label>
|
||
<input type="text" value="${esc(p.betreff)}" oninput="currentProtokollSetzeFeld('betreff', this.value)">
|
||
</div>
|
||
<div class="protokoll-field">
|
||
<label>Ort</label>
|
||
<input type="text" value="${esc(p.ort)}" oninput="currentProtokollSetzeFeld('ort', this.value)">
|
||
</div>
|
||
</div>`;
|
||
|
||
const teilnehmerZeilen = p.teilnehmer.map((t, idx) => `
|
||
<div class="protokoll-teilnehmer-row">
|
||
<input type="text" value="${esc(t.name)}" placeholder="Name" oninput="currentProtokollSetzeTeilnehmerFeld(${idx}, 'name', this.value)">
|
||
<input type="text" value="${esc(t.firma)}" placeholder="Firma / Rolle" oninput="currentProtokollSetzeTeilnehmerFeld(${idx}, 'firma', this.value)">
|
||
<label class="protokoll-anwesend-toggle"><input type="checkbox" ${t.anwesend ? 'checked' : ''} onchange="currentProtokollSetzeTeilnehmerFeld(${idx}, 'anwesend', this.checked)"> anwesend</label>
|
||
<span class="protokoll-row-remove" onclick="entferneTeilnehmer(${idx})" title="Teilnehmer entfernen">✕</span>
|
||
</div>`).join('');
|
||
const teilnehmerBlock = `
|
||
<div class="protokoll-block">
|
||
<div class="protokoll-block-head"><h4>Teilnehmer</h4></div>
|
||
${teilnehmerZeilen || '<span class="hint">Noch keine Teilnehmer.</span>'}
|
||
${locked ? '' : '<div class="add-row-btn" onclick="fuegeTeilnehmerHinzu()">+ Teilnehmer</div>'}
|
||
</div>`;
|
||
|
||
container.innerHTML = `<div class="plan-panel">
|
||
<div class="protokoll-back-link" onclick="schliesseProtokollDetail()">← Zurück zur Liste</div>
|
||
${kopf}
|
||
${teilnehmerBlock}
|
||
<div class="protokoll-block" id="protokollPunkteBlock">${renderPunkteBlockHtml()}</div>
|
||
</div>`;
|
||
}
|
||
|
||
// renderPunkteBlockHtml: baut nur den Punkte-Block — EINE durchgehende
|
||
// Tabelle (dieselben Spalten wie der PDF-Export, js/pdf.js), mit einer
|
||
// vollbreiten Gruppen-Kopfzeile je Abschnitt statt eigener Tabellen je
|
||
// Gruppe — eigene Funktion, weil sie nach einer Gruppenänderung isoliert
|
||
// neu gerendert wird, ohne den Rest der Detailansicht (und damit den Fokus
|
||
// in anderen Feldern) anzufassen.
|
||
// PROTOKOLL_GRUPPEN_FARBEN/protokollGruppeFarbe: der Farbbalken der
|
||
// Gruppen-Kopfzeile wird — wie projektFarbe() in js/load/load.js für die
|
||
// Auslastung — aus dem Gruppennamen gehasht statt fest zugeordnet, damit
|
||
// eine Gruppe ihre Farbe behält, egal an welcher Position sie steht.
|
||
// Eigene, von den Status-Farben (Amber/Grün/Teal/Rot) getrennte Palette,
|
||
// damit eine Gruppenfarbe nie wie ein Status aussieht.
|
||
const PROTOKOLL_GRUPPEN_FARBEN = ['#5b63d6', '#8b5cf6', '#c2419a', '#4f8ef7', '#7a52c7'];
|
||
function protokollGruppeFarbe(name){
|
||
let hash = 0;
|
||
const s = String(name || '');
|
||
for(let i = 0; i < s.length; i++) hash = (hash * 31 + s.charCodeAt(i)) >>> 0;
|
||
return PROTOKOLL_GRUPPEN_FARBEN[hash % PROTOKOLL_GRUPPEN_FARBEN.length];
|
||
}
|
||
|
||
// ausgewaehlterPunkt: der gerade im Schreibbereich geöffnete Punkt, als
|
||
// OBJEKT-Referenz (nicht als Index). Die Punkte wandern beim Verschieben
|
||
// und beim Umsortieren einer ganzen Gruppe durch das Array; eine gemerkte
|
||
// Zahl zeigte danach auf einen anderen Punkt, die Referenz überlebt jede
|
||
// dieser Umstellungen unverändert. Nur ein Speichern tauscht die Objekte
|
||
// aus (der Server liefert frische) — dort wird die Auswahl über ihre
|
||
// Position wiederhergestellt, siehe speichereProtokoll().
|
||
let ausgewaehlterPunkt = null;
|
||
|
||
// waehlePunkt: öffnet einen Punkt im Schreibbereich. Parameter: idx (Index
|
||
// in currentProtokoll.punkte). Keine Rückgabe.
|
||
window.waehlePunkt = function(idx){
|
||
if(!currentProtokoll || !currentProtokoll.punkte[idx]) return;
|
||
ausgewaehlterPunkt = currentProtokoll.punkte[idx];
|
||
const block = document.getElementById('protokollPunkteBlock');
|
||
if(block) block.innerHTML = renderPunkteBlockHtml();
|
||
};
|
||
|
||
// listenUnterzeile: die kleine zweite Zeile eines Listeneintrags —
|
||
// Zuständig und Termin, so weit vorhanden. Parameter: punkt. Rückgabe:
|
||
// String.
|
||
function listenUnterzeile(punkt){
|
||
const teile = [];
|
||
const zust = String(punkt.zustaendig || '').trim();
|
||
if(zust) teile.push(zust);
|
||
const termin = formatDateDE(parseDateISO(punkt.termin));
|
||
if(termin) teile.push(termin);
|
||
return teile.join(' · ') || 'noch nichts erfasst';
|
||
}
|
||
|
||
// aktualisiereListenZeile: schreibt Thema, Zuständig/Termin und den
|
||
// Status-Punkt eines Listeneintrags neu — gezielt am vorhandenen Element,
|
||
// OHNE den Block neu zu rendern. Nötig, weil im Schreibbereich getippter
|
||
// Text sofort links in der Liste erscheinen soll, ein Neu-Rendern dabei
|
||
// aber den Eingabefokus (und die Schreibmarke) zerstören würde — dieselbe
|
||
// Begründung wie im Datei-Kopfkommentar. Parameter: idx. Keine Rückgabe.
|
||
function aktualisiereListenZeile(idx){
|
||
const punkt = currentProtokoll && currentProtokoll.punkte[idx];
|
||
const zeile = document.getElementById('protokollListeZeile_' + idx);
|
||
if(!punkt || !zeile) return;
|
||
const titel = zeile.querySelector('.titel');
|
||
if(titel){
|
||
const thema = String(punkt.thema || '').trim();
|
||
titel.textContent = thema || 'Ohne Thema';
|
||
titel.classList.toggle('ist-leer', !thema);
|
||
}
|
||
const sub = zeile.querySelector('.sub');
|
||
if(sub) sub.textContent = listenUnterzeile(punkt);
|
||
const dot = zeile.querySelector('.protokoll-punkt-dot');
|
||
if(dot) dot.setAttribute('data-status', punkt.status || 'offen');
|
||
}
|
||
|
||
// renderPunkteBlockHtml: baut den Punkte-Block als ZWEI nebeneinander
|
||
// liegende Bereiche (Eriks Auswahl „Liste + Schreibbereich" aus dem
|
||
// Protokoll-schreiben-Mockup) statt der früheren durchgehenden Tabelle:
|
||
// links eine schmale, überfliegbare Liste aller Punkte (nach Gruppen
|
||
// gegliedert, je Zeile Nummer, Thema, Status-Punkt und darunter
|
||
// Zuständig/Termin), rechts der Schreibbereich für den EINEN gerade
|
||
// ausgewählten Punkt mit großem Thema-Feld, mehrzeiligem Textfeld und
|
||
// Zuständig/Termin als ordentlichen Formularfeldern.
|
||
//
|
||
// Warum weg von der Tabelle: ein Protokollpunkt ist ein kleiner Fließtext,
|
||
// keine Tabellenzelle. In der Spalte blieben für den Text rund 60 % der
|
||
// Breite bei drei, vier sichtbaren Zeilen; Thema und Text klebten
|
||
// übereinander, und die Formatierungsleiste saß eingeklemmt dazwischen.
|
||
// Der Schreibbereich gibt dem Text eine lesbare Zeilenlänge und jedem Feld
|
||
// eine eigene Beschriftung.
|
||
//
|
||
// Eigene Funktion, weil sie nach jeder strukturellen Änderung (Punkt/Gruppe
|
||
// anlegen, löschen, verschieben, auswählen) isoliert neu gerendert wird,
|
||
// ohne den Rest der Detailansicht anzufassen. Reine Feldänderungen rendern
|
||
// dagegen NICHT neu, sondern aktualisieren die Liste gezielt über
|
||
// aktualisiereListenZeile() oben.
|
||
function renderPunkteBlockHtml(){
|
||
const p = currentProtokoll;
|
||
const locked = app.protokolleGesperrt();
|
||
const gruppen = gruppenReihenfolge();
|
||
const nummern = window.berechneProtokollNummern(p.punkte);
|
||
|
||
// Auswahl nachziehen: fehlt sie oder zeigt sie auf einen inzwischen
|
||
// gelöschten Punkt, rückt der erste vorhandene nach.
|
||
let aktivIdx = p.punkte.indexOf(ausgewaehlterPunkt);
|
||
if(aktivIdx === -1){
|
||
aktivIdx = p.punkte.length ? 0 : -1;
|
||
ausgewaehlterPunkt = aktivIdx >= 0 ? p.punkte[aktivIdx] : null;
|
||
}
|
||
|
||
const offeneAnzahl = p.punkte.filter(x => (x.status || 'offen') === 'offen').length;
|
||
const kopfText = p.punkte.length
|
||
? `${p.punkte.length} ${p.punkte.length === 1 ? 'Punkt' : 'Punkte'} · ${offeneAnzahl} offen`
|
||
: 'Noch keine Punkte';
|
||
|
||
const listeHtml = gruppen.map(gruppe => {
|
||
const punkteDerGruppe = p.punkte
|
||
.map((punkt, idx) => ({ punkt, idx }))
|
||
.filter(x => String(x.punkt.gruppe || '').trim() === gruppe);
|
||
let moveBtns = '';
|
||
if(!locked){
|
||
const gPos = gruppen.indexOf(gruppe);
|
||
moveBtns = `<span class="protokoll-gruppe-move">
|
||
<button type="button" class="secondary small" ${gPos <= 0 ? 'disabled' : ''} onclick="verschiebeGruppe('${esc(gruppe)}', -1)" title="Gruppe nach vorn">↑</button>
|
||
<button type="button" class="secondary small" ${gPos >= gruppen.length - 1 ? 'disabled' : ''} onclick="verschiebeGruppe('${esc(gruppe)}', 1)" title="Gruppe nach hinten">↓</button>
|
||
</span>`;
|
||
}
|
||
const kopf = `<div class="protokoll-liste-gruppe" style="--protokoll-gruppe-farbe: ${protokollGruppeFarbe(gruppe)}">
|
||
<input class="protokoll-gruppe-heading" value="${esc(gruppe)}" placeholder="Gruppenname …" ${locked ? 'disabled' : ''}
|
||
onchange="currentProtokollUmbenenneGruppe('${esc(gruppe)}', this.value)">
|
||
<span class="count">${punkteDerGruppe.length}</span>${moveBtns}
|
||
</div>`;
|
||
const zeilen = punkteDerGruppe.map(({ punkt, idx }) =>
|
||
renderListenZeileHtml(punkt, idx, nummern[idx], idx === aktivIdx)).join('');
|
||
const addZeile = locked ? '' : `<div class="protokoll-liste-neu" onclick="fuegePunktHinzu('${esc(gruppe)}')">+ Punkt</div>`;
|
||
return kopf + zeilen + addZeile;
|
||
}).join('');
|
||
|
||
const leerHinweis = listeHtml ? '' : '<div class="protokoll-liste-leer">Noch keine Punkte erfasst.</div>';
|
||
const neueGruppe = locked ? '' : `<div class="protokoll-liste-neu ist-gruppe" onclick="fuegeNeueGruppeHinzu()">+ Neue Gruppe</div>`;
|
||
|
||
return `<div class="protokoll-block-head"><h4>Punkte</h4></div>
|
||
<div class="protokoll-split">
|
||
<div class="protokoll-liste">
|
||
<div class="protokoll-liste-kopf">${esc(kopfText)}</div>
|
||
${listeHtml}${leerHinweis}${neueGruppe}
|
||
</div>
|
||
${renderSchreibbereichHtml(aktivIdx, nummern)}
|
||
</div>
|
||
<div class="legend">
|
||
<span class="legend-item"><span class="legend-swatch neu"></span>Neu in diesem Protokoll</span>
|
||
<span class="legend-item"><span class="legend-swatch info"></span>Info (läuft nicht weiter mit)</span>
|
||
</div>`;
|
||
}
|
||
|
||
// renderListenZeileHtml: ein Eintrag der linken Liste. Bewusst knapp —
|
||
// Nummer, Thema, Status-Punkt und darunter Zuständig/Termin; alles
|
||
// Weitere steht im Schreibbereich. Parameter: punkt, idx, nummer,
|
||
// istAktiv. Rückgabe: HTML-String.
|
||
function renderListenZeileHtml(punkt, idx, nummer, istAktiv){
|
||
const thema = String(punkt.thema || '').trim();
|
||
const neuChip = punkt.neu ? '<span class="protokoll-chip-neu">Neu</span>' : '';
|
||
return `<div class="protokoll-liste-zeile${istAktiv ? ' ist-aktiv' : ''}" id="protokollListeZeile_${idx}" onclick="waehlePunkt(${idx})">
|
||
<span class="nr">${esc(nummer)}</span>
|
||
<span class="titel${thema ? '' : ' ist-leer'}">${esc(thema || 'Ohne Thema')}</span>
|
||
<span class="protokoll-punkt-dot" data-status="${esc(punkt.status || 'offen')}" title="${esc(PROTOKOLL_PUNKT_STATUS_LABEL[punkt.status || 'offen'] || 'Offen')}"></span>
|
||
<span class="sub">${esc(listenUnterzeile(punkt))}</span>
|
||
${neuChip}
|
||
</div>`;
|
||
}
|
||
|
||
// PROTOKOLL_PUNKT_STATUS_LABEL: Anzeigetext je Status im Status-Umschalter
|
||
// unten — einzige Stelle, die die drei Werte kennt, damit ein vierter
|
||
// Status (sollte es den je geben) nur hier ergänzt werden müsste.
|
||
const PROTOKOLL_PUNKT_STATUS_LABEL = { offen: 'Offen', erledigt: 'Erledigt', info: 'Info' };
|
||
|
||
// renderSchreibbereichHtml: die rechte Hälfte — der EINE gerade
|
||
// ausgewählte Punkt, mit echtem Platz zum Schreiben. Parameter: idx (Index
|
||
// in currentProtokoll.punkte, -1 = kein Punkt vorhanden), nummern (das
|
||
// Ergebnis von berechneProtokollNummern()). Rückgabe: HTML-String.
|
||
function renderSchreibbereichHtml(idx, nummern){
|
||
const locked = app.protokolleGesperrt();
|
||
if(idx < 0){
|
||
return `<div class="protokoll-schreiben ist-leer">
|
||
<p>Noch kein Punkt angelegt.</p>
|
||
${locked ? '' : '<p class="hint">Links über „+ Neue Gruppe" beginnen — jeder Punkt gehört zu einer Gruppe.</p>'}
|
||
</div>`;
|
||
}
|
||
const punkt = currentProtokoll.punkte[idx];
|
||
const terminDisplay = formatDateDE(parseDateISO(punkt.termin)) || '';
|
||
const neuChip = punkt.neu ? '<span class="protokoll-chip-neu">Neu</span>' : '';
|
||
// entfaelltNaechstes: vom Server gesetzt, wenn dieser Punkt beim
|
||
// Fortschreiben aus einem erledigten Punkt des vorigen Protokolls
|
||
// entstand — seine Gnadenrunde (Eriks Wunsch: ein erledigter Punkt
|
||
// taucht noch EIN Protokoll lang auf, bevor er entfällt). Nie vom
|
||
// Client gesetzt (siehe PUT .../:id, server.js).
|
||
const entfaelltChip = punkt.entfaelltNaechstes
|
||
? '<span class="protokoll-chip-entfaellt" title="Dieser Punkt wurde bereits als erledigt fortgeschrieben — im übernächsten Protokoll taucht er nicht mehr auf.">Entfällt im nächsten Protokoll</span>'
|
||
: '';
|
||
// themaGeaendert/textGeaendert: vom Server gesetzt (siehe
|
||
// loadProtokollDetail(), server.js) für einen fortgeschriebenen Punkt
|
||
// (neu=0), dessen Thema bzw. Text sich gegenüber der Fassung im
|
||
// UNMITTELBAR vorangegangenen Protokoll geändert hat — markiert NUR das
|
||
// jeweils geänderte Feld, der Punkt selbst ist ja derselbe geblieben.
|
||
const themaGeaendertClass = punkt.themaGeaendert ? ' ist-geaendert' : '';
|
||
const textGeaendertClass = punkt.textGeaendert ? ' ist-geaendert' : '';
|
||
const status = punkt.status || 'offen';
|
||
|
||
// Verschieben gilt wie bisher nur INNERHALB der eigenen Gruppe — die
|
||
// Knöpfe sind an den Rändern dieser Gruppe gesperrt, nicht an denen der
|
||
// Gesamtliste.
|
||
const gruppe = String(punkt.gruppe || '').trim();
|
||
const idxDerGruppe = currentProtokoll.punkte
|
||
.map((x, i) => ({ x, i }))
|
||
.filter(e => String(e.x.gruppe || '').trim() === gruppe)
|
||
.map(e => e.i);
|
||
const posInGruppe = idxDerGruppe.indexOf(idx);
|
||
const kopfAktionen = locked ? '' : `<span class="protokoll-punkt-move">
|
||
<button type="button" class="secondary small" ${posInGruppe <= 0 ? 'disabled' : ''} onclick="verschiebePunkt(${idx}, -1)" title="Innerhalb der Gruppe nach oben">↑</button>
|
||
<button type="button" class="secondary small" ${posInGruppe >= idxDerGruppe.length - 1 ? 'disabled' : ''} onclick="verschiebePunkt(${idx}, 1)" title="Innerhalb der Gruppe nach unten">↓</button>
|
||
</span>
|
||
<span class="protokoll-row-remove" onclick="entfernePunkt(${idx})" title="Punkt entfernen">✕</span>`;
|
||
|
||
const statusButtons = Object.keys(PROTOKOLL_PUNKT_STATUS_LABEL).map(s =>
|
||
`<button type="button" class="${s}${s === status ? ' is-active' : ''}" ${locked ? 'disabled' : ''}
|
||
onclick="currentProtokollSetzePunktStatus(${idx}, '${s}')">${PROTOKOLL_PUNKT_STATUS_LABEL[s]}</button>`).join('');
|
||
|
||
return `<div class="protokoll-schreiben">
|
||
<div class="protokoll-schreiben-kopf">
|
||
<span class="nr" title="Errechnet sich automatisch aus Gruppe und Reihenfolge">${esc(nummern[idx])}</span>
|
||
<span class="gruppe">${esc(gruppe)}</span>
|
||
${neuChip}${entfaelltChip}
|
||
<span class="rechts">
|
||
<div class="protokoll-status-switch ist-waagerecht">${statusButtons}</div>
|
||
${kopfAktionen}
|
||
</span>
|
||
</div>
|
||
|
||
<div class="protokoll-schreiben-feld">
|
||
<span class="protokoll-feld-label">Thema</span>
|
||
<input type="text" class="protokoll-punkt-thema${themaGeaendertClass}" value="${esc(punkt.thema)}"
|
||
placeholder="Worum geht es?" ${locked ? 'disabled' : ''}
|
||
oninput="currentProtokollSetzePunktFeld(${idx}, 'thema', this.value)">
|
||
</div>
|
||
|
||
<div class="richtext-wrap">
|
||
${locked ? '' : richtextToolbarHtml('.protokoll-punkt-text')}
|
||
<div class="protokoll-schreiben-feld">
|
||
<span class="protokoll-feld-label">Vereinbarung · Text</span>
|
||
${richtextFeldHtml('protokoll-punkt-text' + textGeaendertClass, sanitizeStatusHtml(punkt.text),
|
||
'Was wurde besprochen oder vereinbart?',
|
||
`oninput="currentProtokollSetzePunktFeld(${idx}, 'text', this.innerHTML)"`, !locked)}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="protokoll-schreiben-raster">
|
||
<div>
|
||
<span class="protokoll-feld-label">Zuständig</span>
|
||
<input type="text" value="${esc(punkt.zustaendig)}" placeholder="Name oder Firma" ${locked ? 'disabled' : ''}
|
||
oninput="currentProtokollSetzePunktFeld(${idx}, 'zustaendig', this.value)">
|
||
</div>
|
||
<div>
|
||
<span class="protokoll-feld-label">Termin</span>
|
||
<input type="text" id="protokollPunktTerminDisplay_${idx}" class="range-picker-display" value="${esc(terminDisplay)}" placeholder="— kein Termin" readonly
|
||
onclick="${locked ? '' : `openDatePicker({ anchor: this, inputId: 'protokollPunktTermin_${idx}', displayId: 'protokollPunktTerminDisplay_${idx}', onApply: function(){ syncProtokollPunktTermin(${idx}); } })`}">
|
||
<input type="hidden" id="protokollPunktTermin_${idx}" value="${esc(punkt.termin)}">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="protokoll-schreiben-fuss">
|
||
Erstellt am ${esc(formatDateDE(parseDateISO(punkt.erstelltAm)) || '—')}
|
||
<span class="hint">— bleibt beim Fortschreiben unverändert</span>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
// ---------- Feld-Mutationen (ohne Neu-Rendern, siehe Datei-Kopfkommentar) ----------
|
||
|
||
window.currentProtokollSetzeFeld = function(feld, wert){
|
||
if(!currentProtokoll) return;
|
||
currentProtokoll[feld] = wert;
|
||
};
|
||
window.syncProtokollDatum = function(){
|
||
if(!currentProtokoll) return;
|
||
currentProtokoll.datum = document.getElementById('protokollDatum').value;
|
||
};
|
||
window.currentProtokollSetzeTeilnehmerFeld = function(idx, feld, wert){
|
||
if(!currentProtokoll || !currentProtokoll.teilnehmer[idx]) return;
|
||
currentProtokoll.teilnehmer[idx][feld] = wert;
|
||
};
|
||
// Thema/Zuständig schlagen sofort auf den Listeneintrag links durch, ohne
|
||
// den Block neu zu rendern (sonst wäre der Fokus beim Tippen weg, siehe
|
||
// aktualisiereListenZeile() oben).
|
||
window.currentProtokollSetzePunktFeld = function(idx, feld, wert){
|
||
if(!currentProtokoll || !currentProtokoll.punkte[idx]) return;
|
||
currentProtokoll.punkte[idx][feld] = wert;
|
||
if(feld === 'thema' || feld === 'zustaendig') aktualisiereListenZeile(idx);
|
||
};
|
||
window.syncProtokollPunktTermin = function(idx){
|
||
if(!currentProtokoll || !currentProtokoll.punkte[idx]) return;
|
||
const el = document.getElementById('protokollPunktTermin_' + idx);
|
||
if(el) currentProtokoll.punkte[idx].termin = el.value;
|
||
aktualisiereListenZeile(idx);
|
||
};
|
||
window.currentProtokollSetzePunktStatus = function(idx, status){
|
||
if(!currentProtokoll || !currentProtokoll.punkte[idx]) return;
|
||
currentProtokoll.punkte[idx].status = status;
|
||
// Gezielt die beiden betroffenen Stellen nachziehen statt neu zu rendern:
|
||
// der aktive Knopf im Umschalter und der farbige Punkt im Listeneintrag.
|
||
document.querySelectorAll('.protokoll-schreiben .protokoll-status-switch button').forEach(btn => {
|
||
btn.classList.toggle('is-active', btn.classList.contains(status));
|
||
});
|
||
aktualisiereListenZeile(idx);
|
||
};
|
||
// currentProtokollUmbenenneGruppe: ersetzt currentProtokollSetzePunktGruppe
|
||
// (bis eben: ein Feld PRO Punkt, das genau diesen einen Punkt umhängte).
|
||
// Jetzt ist die Gruppen-Kopfzeile selbst das Eingabefeld (Eriks Wunsch) —
|
||
// ein Umbenennen wirkt auf ALLE Punkte dieser Gruppe gleichzeitig. Trägt
|
||
// der neue Name den einer bereits bestehenden anderen Gruppe, verschmelzen
|
||
// beide Gruppen schlicht dadurch, dass ab jetzt beide Punktmengen denselben
|
||
// gruppe-Wert tragen — kein Sonderfall nötig. Läuft über onchange
|
||
// (Blur/Enter), nicht oninput, damit während des Tippens nicht bei jedem
|
||
// Zeichen neu gerendert wird.
|
||
window.currentProtokollUmbenenneGruppe = function(alterName, neuerName){
|
||
if(!currentProtokoll) return;
|
||
const alt = String(alterName || '').trim();
|
||
const neu = String(neuerName || '').trim();
|
||
if(alt === neu) return;
|
||
currentProtokoll.punkte.forEach(punkt => {
|
||
if(String(punkt.gruppe || '').trim() === alt) punkt.gruppe = neu;
|
||
});
|
||
const block = document.getElementById('protokollPunkteBlock');
|
||
if(block) block.innerHTML = renderPunkteBlockHtml();
|
||
};
|
||
|
||
// ---------- Strukturelle Änderungen (Zeile hinzufügen/entfernen) ----------
|
||
|
||
window.fuegeTeilnehmerHinzu = function(){
|
||
if(!currentProtokoll || app.protokolleGesperrt()) return;
|
||
currentProtokoll.teilnehmer.push({ name: '', firma: '', anwesend: true });
|
||
renderProtokollDetail();
|
||
};
|
||
window.entferneTeilnehmer = function(idx){
|
||
if(!currentProtokoll || app.protokolleGesperrt()) return;
|
||
currentProtokoll.teilnehmer.splice(idx, 1);
|
||
renderProtokollDetail();
|
||
};
|
||
window.fuegePunktHinzu = function(gruppe){
|
||
if(!currentProtokoll || app.protokolleGesperrt()) return;
|
||
// nummer wird nicht gesetzt — sie ergibt sich beim Rendern automatisch
|
||
// aus Gruppe und Position (siehe berechneProtokollNummern() oben).
|
||
// erstelltAm nur als Vorschau bis zum nächsten Speichern — maßgeblich ist
|
||
// immer, was der Server beim Speichern setzt (server.js, PUT .../:id).
|
||
const neuerPunkt = {
|
||
id: null, nummer: '', gruppe: gruppe || '', neu: true,
|
||
thema: '', text: '', zustaendig: '', termin: '', status: 'offen',
|
||
erstelltAm: currentProtokoll.datum || '', entfaelltNaechstes: false
|
||
};
|
||
// Hinter dem letzten Punkt DERSELBEN Gruppe einfügen statt ans Ende: Die
|
||
// Nummern folgen der Reihenfolge im Array (berechneProtokollNummern()),
|
||
// die Anzeige aber gruppiert. Ein ans Ende gehängter Punkt "ohne Gruppe"
|
||
// bekam sonst eine Nummer hinter allen Gruppen (1–5, dann 7, darunter 6.1).
|
||
// Gibt es noch keinen Punkt ohne Gruppe, kommt er an den Anfang — dieser
|
||
// Abschnitt steht immer zuerst.
|
||
const liste = currentProtokoll.punkte;
|
||
const g = gruppe || '';
|
||
let letzter = -1;
|
||
liste.forEach((x, i) => { if((x.gruppe || '') === g) letzter = i; });
|
||
if(letzter >= 0) liste.splice(letzter + 1, 0, neuerPunkt);
|
||
else if(!g) liste.unshift(neuerPunkt);
|
||
else liste.push(neuerPunkt);
|
||
// Den neuen Punkt gleich im Schreibbereich öffnen und ins Thema-Feld
|
||
// springen — sonst stünde er zwar links in der Liste, rechts bliebe
|
||
// aber der vorige Punkt stehen und man müsste ihn erst anklicken.
|
||
ausgewaehlterPunkt = neuerPunkt;
|
||
const block = document.getElementById('protokollPunkteBlock');
|
||
if(block) block.innerHTML = renderPunkteBlockHtml();
|
||
const themaFeld = block && block.querySelector('.protokoll-schreiben .protokoll-punkt-thema');
|
||
if(themaFeld) themaFeld.focus();
|
||
};
|
||
// fuegeNeueGruppeHinzu: eigener, ausdrücklicher Weg, eine neue Gruppe
|
||
// anzulegen (Eriks Wunsch). Gruppe bleibt weiterhin freier Text ohne eigene
|
||
// Tabelle (siehe CLAUDE.md "### Protokolle") — eine Gruppe ohne jeden
|
||
// Punkt hätte in diesem Modell nirgends eine Repräsentation, deshalb legt
|
||
// dieser Knopf die Gruppe zusammen mit ihrem ersten (leeren) Punkt an. Ein
|
||
// nicht kollidierender Platzhaltername ("Neue Gruppe", bei Bedarf
|
||
// "Neue Gruppe 2" …) wird direkt danach in der großen Gruppen-Überschrift
|
||
// (nicht mehr einem Feld am Punkt, siehe currentProtokollUmbenenneGruppe()
|
||
// oben) fokussiert und markiert, damit er sich sofort überschreiben lässt.
|
||
window.fuegeNeueGruppeHinzu = function(){
|
||
if(!currentProtokoll || app.protokolleGesperrt()) return;
|
||
const bestehende = new Set(gruppenReihenfolge());
|
||
let name = 'Neue Gruppe';
|
||
let n = 2;
|
||
while(bestehende.has(name)){ name = 'Neue Gruppe ' + n; n++; }
|
||
fuegePunktHinzu(name);
|
||
const block = document.getElementById('protokollPunkteBlock');
|
||
const input = block
|
||
? Array.from(block.querySelectorAll('.protokoll-gruppe-heading')).find(el => el.value === name)
|
||
: null;
|
||
if(input){ input.focus(); input.select(); }
|
||
};
|
||
window.entfernePunkt = function(idx){
|
||
if(!currentProtokoll || app.protokolleGesperrt()) return;
|
||
// War der gelöschte Punkt der geöffnete, rückt der an seine Stelle
|
||
// getretene nach (bzw. der letzte, wenn es der letzte war) — sonst
|
||
// stünde der Schreibbereich leer da, obwohl noch Punkte vorhanden sind.
|
||
const warAusgewaehlt = currentProtokoll.punkte[idx] === ausgewaehlterPunkt;
|
||
currentProtokoll.punkte.splice(idx, 1);
|
||
if(warAusgewaehlt){
|
||
const ersatz = Math.min(idx, currentProtokoll.punkte.length - 1);
|
||
ausgewaehlterPunkt = ersatz >= 0 ? currentProtokoll.punkte[ersatz] : null;
|
||
}
|
||
const block = document.getElementById('protokollPunkteBlock');
|
||
if(block) block.innerHTML = renderPunkteBlockHtml();
|
||
};
|
||
|
||
// ---------- Speichern / Löschen ----------
|
||
|
||
// speichereProtokoll: PUT /project-protokolle/:id — ersetzt Teilnehmer und
|
||
// Punkte komplett (siehe server.js). Ausdrückliches Speichern, kein
|
||
// Auto-Save, wie die Board-Kommentare.
|
||
window.speichereProtokoll = function(){
|
||
if(!currentProtokoll || app.protokolleGesperrt()) return;
|
||
// Errechnete Nummern vor dem Senden in die Punkte schreiben, damit die
|
||
// gespeicherten Werte mit der Anzeige übereinstimmen (maßgeblich ist beim
|
||
// Anzeigen aber immer die Neuberechnung, nie dieser gespeicherte Wert —
|
||
// siehe berechneProtokollNummern()).
|
||
const nummern = window.berechneProtokollNummern(currentProtokoll.punkte);
|
||
// text kommt live aus dem contenteditable-Feld (this.innerHTML,
|
||
// currentProtokollSetzePunktFeld()) — ein Einfügen von außerhalb könnte
|
||
// dort ungefiltertes HTML hinterlassen (contenteditable übernimmt
|
||
// eingefügte Formatierung standardmäßig); vor dem Senden deshalb
|
||
// dieselbe Bereinigung wie beim Board-Kommentar (saveBoardComment(),
|
||
// js/board/board.js).
|
||
currentProtokoll.punkte.forEach((p, idx) => {
|
||
p.nummer = nummern[idx];
|
||
p.text = sanitizeStatusHtml(p.text || '');
|
||
});
|
||
const body = {
|
||
datum: currentProtokoll.datum,
|
||
betreff: currentProtokoll.betreff,
|
||
ort: currentProtokoll.ort,
|
||
teilnehmer: currentProtokoll.teilnehmer.filter(t => String(t.name || '').trim()),
|
||
punkte: currentProtokoll.punkte
|
||
};
|
||
// Die Auswahl hängt an der OBJEKT-Referenz des Punkts (siehe dort) — der
|
||
// Server liefert gleich frische Objekte zurück, die Referenz zeigt dann
|
||
// ins Leere. Die Reihenfolge bleibt beim Speichern erhalten (array_order),
|
||
// also lässt sich die Auswahl über ihre Position wiederherstellen.
|
||
const auswahlPos = currentProtokoll.punkte.indexOf(ausgewaehlterPunkt);
|
||
fetch(baseUrl() + '/project-protokolle/' + encodeURIComponent(currentProtokoll.id), {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
||
body: JSON.stringify(body)
|
||
})
|
||
.then(r => r.json().then(d => ({ status: r.status, ok: r.ok, d })))
|
||
.then(({ ok, d }) => {
|
||
if(!ok || !d.ok){ alert('Protokoll konnte nicht gespeichert werden: ' + (d && d.error ? d.error : 'unbekannter Fehler')); return; }
|
||
currentProtokoll = d.protokoll;
|
||
ausgewaehlterPunkt = (auswahlPos >= 0 && currentProtokoll.punkte[auswahlPos]) || null;
|
||
renderProtokollDetail();
|
||
ladeProtokollListe();
|
||
})
|
||
.catch(err => {
|
||
console.error('Protokoll speichern fehlgeschlagen:', err);
|
||
alert('Protokoll konnte nicht gespeichert werden: ' + (err.message || err));
|
||
});
|
||
};
|
||
|
||
window.loescheProtokoll = function(){
|
||
if(!currentProtokoll || app.protokolleGesperrt()) return;
|
||
if(!confirm('Protokoll Nr. ' + currentProtokoll.nummer + ' wirklich löschen?')) return;
|
||
fetch(baseUrl() + '/project-protokolle/' + encodeURIComponent(currentProtokoll.id), { method: 'DELETE' })
|
||
.then(r => { if(!r.ok) throw new Error('Server HTTP ' + r.status); return r.json(); })
|
||
.then(d => {
|
||
if(!d.ok){ alert('Protokoll konnte nicht gelöscht werden.'); return; }
|
||
currentProtokoll = null;
|
||
ladeProtokollListe();
|
||
})
|
||
.catch(err => {
|
||
console.error('Protokoll löschen fehlgeschlagen:', err);
|
||
alert('Protokoll konnte nicht gelöscht werden: ' + (err.message || err));
|
||
});
|
||
};
|
||
|
||
// Die Rückmeldung der Formatierungsleiste (hervorgehoben + "✓ Markiert"
|
||
// sobald Text ausgewählt ist, sonst blass + "Erst Text markieren") ist ein
|
||
// gemeinsamer, dokumentweiter Lauscher in js/utils.js geworden (gilt jetzt
|
||
// auch für den Board-Kommentar) — hier war er ursprünglich fest auf den
|
||
// Protokoll-Schreibbereich zugeschnitten.
|
||
|
||
// renderPlanProtokolle: zentrale Render-Funktion, von showPlanSection()
|
||
// (plan-sections.js) beim Wechsel in den Bereich aufgerufen — zeigt je
|
||
// nach Zustand Liste oder Detail (rendert dabei nichts neu, nur aus dem
|
||
// vorhandenen protokolleCache/currentProtokoll).
|
||
window.renderPlanProtokolle = function(){
|
||
if(currentProtokoll) renderProtokollDetail();
|
||
else renderProtokolleListe();
|
||
};
|
||
})();
|