// ----------------------------- // 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 = 'Lade Protokolle…'; 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 = 'Protokolle konnten nicht geladen werden.'; 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 = `
Noch keine Protokolle.
`; return; } const letztes = protokolleCache[0]; // Server liefert nach nummer DESC const frueher = protokolleCache.slice(1, 1 + DASH_PROTOKOLL_FRUEHERE); const zahlen = [ letztes.offenCount ? `${letztes.offenCount} offen` : 'nichts offen', letztes.erledigtCount ? `${letztes.erledigtCount} erledigt` : '', letztes.infoCount ? `${letztes.infoCount} Info` : '' ].join(''); el.innerHTML = `
${zahlen}
${letztes.offenCount ? 'Lade offene Punkte…' : ''}
${frueher.length ? `
Frühere Protokolle
${frueher.map(pr => ``).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 `
${esc(p.anzeigeNr)} ${esc(titel)}${p.gruppe ? esc(p.gruppe) + ' · ' : ''}${p.zustaendig ? esc(p.zustaendig) : 'ohne Zuständigen'} ${p.termin ? esc(datumDe(p.termin)) + (typeof relativerTag === 'function' ? '' + esc(relativerTag(p.termin)) + '' : '') : '—'}
`; }).join('') + (rest > 0 ? `` : ''); }) .catch(err => { if(nr !== dashProtokollAbruf) return; const ziel = document.getElementById('planDashProtokollPunkte'); if(ziel) ziel.innerHTML = 'Offene Punkte konnten nicht geladen werden.'; 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 ? '' : ` `; if(protokolleCache.length === 0){ container.innerHTML = `

Protokolle

${anlegeBtns}
${locked ? 'Für diese Sitzung sind keine Protokolle hinterlegt.' : 'Noch keine Protokolle. Über „+ Neues Protokoll" anlegen oder ein bestehendes importieren.'}
`; return; } const zeilen = protokolleCache.map(p => `
Nr. ${esc(p.nummer)} ${esc(p.betreff || 'ohne Betreff')}${esc(formatDateDE(parseDateISO(p.datum)) || '')}${p.ort ? ' · ' + esc(p.ort) : ''} ${esc(p.verfasserName || '')} ${esc(todoStatusZeile(p.offenCount, p.erledigtCount))} ${p.teilnehmerCount} Teilnehmer
`).join(''); container.innerHTML = `

Protokolle

${anlegeBtns}
${zeilen}
`; } // ---------- 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 = ``; fetch(baseUrl() + '/my-ai-settings') .then(r => r.json()) .then(d => { if(!d.ok) return; erkennungSel.innerHTML = ` `; }) .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 = 'Lade Protokoll…'; 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 = 'Protokoll konnte nicht geladen werden.'; }); }; // 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 ${esc(p.quelleDateiname)}` : ''; // 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 ? `
Punkte automatisch aus „${esc(p.quelleDateiname)}“ erkannt — bitte vor dem Speichern prüfen und bei Bedarf korrigieren.
` : ''; // 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 ? `
Die KI-Erkennung ist fehlgeschlagen (${esc(p._kiFallbackHinweis)}) — die Punkte wurden stattdessen per Textmuster erkannt.
` : ''; const kopf = ` ${kiFallbackBanner} ${importBanner}

${esc(p.betreff || 'ohne Betreff')}

Protokoll Nr. ${esc(p.nummer)} · verfasst von ${esc(p.verfasserName || '—')}${quelleHinweis}
${locked ? '' : ``} ${locked ? '' : ``} ${locked ? '' : ``}
`; const teilnehmerZeilen = p.teilnehmer.map((t, idx) => `
✕
`).join(''); const teilnehmerBlock = `

Teilnehmer

${teilnehmerZeilen || 'Noch keine Teilnehmer.'} ${locked ? '' : '
+ Teilnehmer
'}
`; container.innerHTML = `
${kopf} ${teilnehmerBlock}
${renderPunkteBlockHtml()}
`; } // 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 = ` `; } const kopf = `
${punkteDerGruppe.length}${moveBtns}
`; const zeilen = punkteDerGruppe.map(({ punkt, idx }) => renderListenZeileHtml(punkt, idx, nummern[idx], idx === aktivIdx)).join(''); const addZeile = locked ? '' : `
+ Punkt
`; return kopf + zeilen + addZeile; }).join(''); const leerHinweis = listeHtml ? '' : '
Noch keine Punkte erfasst.
'; const neueGruppe = locked ? '' : `
+ Neue Gruppe
`; return `

Punkte

${esc(kopfText)}
${listeHtml}${leerHinweis}${neueGruppe}
${renderSchreibbereichHtml(aktivIdx, nummern)}
Neu in diesem Protokoll Info (läuft nicht weiter mit)
`; } // 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 ? 'Neu' : ''; return `
${esc(nummer)} ${esc(thema || 'Ohne Thema')} ${esc(listenUnterzeile(punkt))} ${neuChip}
`; } // 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 `

Noch kein Punkt angelegt.

${locked ? '' : '

Links über „+ Neue Gruppe" beginnen — jeder Punkt gehört zu einer Gruppe.

'}
`; } const punkt = currentProtokoll.punkte[idx]; const terminDisplay = formatDateDE(parseDateISO(punkt.termin)) || ''; const neuChip = punkt.neu ? 'Neu' : ''; // 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 ? 'Entfällt im nächsten Protokoll' : ''; // 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 ? '' : ` ✕`; const statusButtons = Object.keys(PROTOKOLL_PUNKT_STATUS_LABEL).map(s => ``).join(''); return `
${esc(nummern[idx])} ${esc(gruppe)} ${neuChip}${entfaelltChip}
${statusButtons}
${kopfAktionen}
Thema
${locked ? '' : richtextToolbarHtml('.protokoll-punkt-text')}
Vereinbarung · Text ${richtextFeldHtml('protokoll-punkt-text' + textGeaendertClass, sanitizeStatusHtml(punkt.text), 'Was wurde besprochen oder vereinbart?', `oninput="currentProtokollSetzePunktFeld(${idx}, 'text', this.innerHTML)"`, !locked)}
Zuständig
Termin
Erstellt am ${esc(formatDateDE(parseDateISO(punkt.erstelltAm)) || '—')} — bleibt beim Fortschreiben unverändert
`; } // ---------- 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(); }; })();