All new
alle new
This commit is contained in:
1 parent
da64472251
commit
5b20adee48
1605 files changed
+396517
-1674
No files matched your search
@@ -0,0 +1,764 @@
|
||||
// -----------------------------
|
||||
// Projektplan — Protokolle (Besprechungsprotokolle je Projekt)
|
||||
// -----------------------------
|
||||
// Bereich "Protokolle" des Projektbereichs (siehe js/plan/plan-sections.js)
|
||||
// plus eine Kompakt-Karte im Dashboard (#planProtokolleSummary, 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 Kompakt-Vorschau der Dashboard-Karte
|
||||
// (#planProtokolleSummary) — letztes Protokoll + Anzahl offener Punkte,
|
||||
// gleiches Muster wie renderFilesSummary() in plan-files.js.
|
||||
function renderProtokolleSummary(){
|
||||
const el = document.getElementById('planProtokolleSummary');
|
||||
if(!el) return;
|
||||
if(!app.project || !app.project.nr){
|
||||
el.innerHTML = '<span class="hint">Keine Projektnummer.</span>';
|
||||
return;
|
||||
}
|
||||
if(protokolleCache.length === 0){
|
||||
el.innerHTML = '<span class="hint">Noch keine Protokolle.</span>';
|
||||
return;
|
||||
}
|
||||
const letztes = protokolleCache[0]; // Server liefert nach nummer DESC
|
||||
const chip = letztes.offenCount > 0
|
||||
? `<span class="plan-mini-chip warn">${letztes.offenCount} offen</span>`
|
||||
: '<span class="plan-mini-chip">Alle erledigt</span>';
|
||||
el.innerHTML = `<div class="plan-mini-card-preview">Nr. ${esc(letztes.nummer)} · ${esc(letztes.betreff || 'ohne Betreff')}</div>
|
||||
<div class="plan-mini-chip-row">${chip}</div>`;
|
||||
}
|
||||
|
||||
// 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) —
|
||||
// "Ohne Gruppe" (leerer String) immer zuerst, wenn vorhanden.
|
||||
function gruppenReihenfolge(){
|
||||
const namen = [];
|
||||
let hatOhneGruppe = false;
|
||||
(currentProtokoll.punkte || []).forEach(p => {
|
||||
const g = String(p.gruppe || '').trim();
|
||||
if(!g){ hatOhneGruppe = true; return; }
|
||||
if(!namen.includes(g)) namen.push(g);
|
||||
});
|
||||
return (hatOhneGruppe ? [''] : []).concat(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 (bzw. innerhalb
|
||||
// "Ohne 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.
|
||||
// "Ohne Gruppe" bleibt bewusst immer der erste Abschnitt (siehe
|
||||
// gruppenReihenfolge()) und ist deshalb hier nicht mitverschiebbar.
|
||||
|
||||
// verschiebePunkt: vertauscht einen Punkt mit seinem Vorgänger/Nachfolger
|
||||
// INNERHALB derselben Gruppe (bzw. "Ohne 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.isLocked()) 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 neu auf: "Ohne Gruppe" zuerst (unverändert),
|
||||
// danach jede Gruppe in der neuen Reihenfolge. Parameter: gruppe (Name),
|
||||
// richtung (-1 = nach vorn, +1 = nach hinten). Keine Rückgabe.
|
||||
window.verschiebeGruppe = function(gruppe, richtung){
|
||||
if(!currentProtokoll || app.isLocked()) return;
|
||||
const punkte = currentProtokoll.punkte;
|
||||
const namen = [];
|
||||
punkte.forEach(p => {
|
||||
const g = String(p.gruppe || '').trim();
|
||||
if(g && !namen.includes(g)) namen.push(g);
|
||||
});
|
||||
const pos = namen.indexOf(gruppe);
|
||||
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 = punkte.filter(p => !String(p.gruppe || '').trim());
|
||||
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.isLocked();
|
||||
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.isLocked()) 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.isLocked()) 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 =
|
||||
// 5 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.isLocked();
|
||||
|
||||
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 (in Abschnitten je
|
||||
// Gruppe) — eigene Funktion, weil er nach einer Gruppenänderung isoliert
|
||||
// neu gerendert wird, ohne den Rest der Detailansicht (und damit den Fokus
|
||||
// in anderen Feldern) anzufassen.
|
||||
function renderPunkteBlockHtml(){
|
||||
const p = currentProtokoll;
|
||||
const locked = app.isLocked();
|
||||
const gruppen = gruppenReihenfolge();
|
||||
const gruppenNamen = gruppen.filter(g => g); // ohne '' für die Datalist-Vorschläge
|
||||
|
||||
const nummern = window.berechneProtokollNummern(p.punkte);
|
||||
const gruppenHtml = gruppen.map(gruppe => {
|
||||
const punkteDerGruppe = p.punkte
|
||||
.map((punkt, idx) => ({ punkt, idx }))
|
||||
.filter(x => String(x.punkt.gruppe || '').trim() === gruppe);
|
||||
const titel = gruppe ? esc(gruppe) : 'Ohne Gruppe';
|
||||
const addBtn = locked ? '' : `<span class="protokoll-gruppe-add" onclick="fuegePunktHinzu('${esc(gruppe)}')">+ Punkt</span>`;
|
||||
// "Ohne Gruppe" bleibt fest der erste Abschnitt (siehe gruppenReihenfolge()) —
|
||||
// nur echte Gruppen lassen sich gegeneinander verschieben.
|
||||
let moveBtns = '';
|
||||
if(gruppe && !locked){
|
||||
const gPos = gruppenNamen.indexOf(gruppe);
|
||||
const hochDisabled = gPos <= 0 ? 'disabled' : '';
|
||||
const runterDisabled = gPos >= gruppenNamen.length - 1 ? 'disabled' : '';
|
||||
moveBtns = `<span class="protokoll-gruppe-move">
|
||||
<button type="button" class="secondary small" ${hochDisabled} onclick="verschiebeGruppe('${esc(gruppe)}', -1)" title="Gruppe nach vorn">↑</button>
|
||||
<button type="button" class="secondary small" ${runterDisabled} onclick="verschiebeGruppe('${esc(gruppe)}', 1)" title="Gruppe nach hinten">↓</button>
|
||||
</span>`;
|
||||
}
|
||||
const zeilen = punkteDerGruppe.map(({ punkt, idx }, pos) => renderPunktZeileHtml(punkt, idx, nummern[idx], pos === 0, pos === punkteDerGruppe.length - 1)).join('');
|
||||
return `<div class="protokoll-gruppe">
|
||||
<div class="protokoll-gruppe-head"><span class="protokoll-gruppe-name${gruppe ? '' : ' is-leer'}">${titel}</span>${moveBtns}${addBtn}</div>
|
||||
${zeilen || '<span class="hint">Keine Punkte in dieser Gruppe.</span>'}
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
const datalist = `<datalist id="protokollGruppenListe">${gruppenNamen.map(g => `<option value="${esc(g)}">`).join('')}</datalist>`;
|
||||
const addOhneGruppe = locked ? '' : `<div class="add-row-btn" onclick="fuegePunktHinzu('')">+ Punkt (ohne Gruppe)</div>`;
|
||||
|
||||
return `<div class="protokoll-block-head"><h4>Punkte</h4></div>
|
||||
${gruppenHtml || '<span class="hint">Noch keine Punkte.</span>'}
|
||||
${datalist}
|
||||
${addOhneGruppe}
|
||||
<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 fortgeschrieben"></span>Aus vorigem Protokoll fortgeschrieben</span>
|
||||
<span class="legend-item"><span class="legend-swatch erledigt"></span>Erledigt (läuft nicht weiter mit)</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderPunktZeileHtml(punkt, idx, nummer, istErste, istLetzte){
|
||||
const locked = app.isLocked();
|
||||
const terminDisplay = formatDateDE(parseDateISO(punkt.termin)) || '';
|
||||
const neuChip = punkt.neu ? '<span class="protokoll-chip-neu">Neu</span>' : '';
|
||||
const moveBtns = locked ? '' : `<span class="protokoll-punkt-move">
|
||||
<button type="button" class="secondary small" ${istErste ? 'disabled' : ''} onclick="verschiebePunkt(${idx}, -1)" title="Innerhalb der Gruppe nach oben">↑</button>
|
||||
<button type="button" class="secondary small" ${istLetzte ? 'disabled' : ''} onclick="verschiebePunkt(${idx}, 1)" title="Innerhalb der Gruppe nach unten">↓</button>
|
||||
</span>`;
|
||||
return `<div class="protokoll-punkt${punkt.neu ? ' is-neu' : ''}${punkt.erledigt ? ' is-erledigt' : ''}" id="protokollPunkt_${idx}">
|
||||
<div class="protokoll-punkt-top-row">
|
||||
<span class="protokoll-punkt-nr" title="Errechnet sich automatisch aus Gruppe und Reihenfolge">${esc(nummer)}</span>
|
||||
${moveBtns}
|
||||
<input type="text" class="protokoll-punkt-gruppe" list="protokollGruppenListe" value="${esc(punkt.gruppe)}"
|
||||
placeholder="Gruppe (z. B. ELT, HLS) …" ${locked ? 'disabled' : ''}
|
||||
onchange="currentProtokollSetzePunktGruppe(${idx}, this.value)">
|
||||
${neuChip}
|
||||
<input type="text" class="protokoll-punkt-thema" value="${esc(punkt.thema)}" placeholder="Thema" ${locked ? 'disabled' : ''}
|
||||
oninput="currentProtokollSetzePunktFeld(${idx}, 'thema', this.value)">
|
||||
<label class="protokoll-punkt-erledigt-toggle"><input type="checkbox" ${punkt.erledigt ? 'checked' : ''} ${locked ? 'disabled' : ''}
|
||||
onchange="currentProtokollSetzePunktErledigt(${idx}, this.checked)"> erledigt</label>
|
||||
${locked ? '' : `<span class="protokoll-row-remove" onclick="entfernePunkt(${idx})" title="Punkt entfernen">✕</span>`}
|
||||
</div>
|
||||
<textarea class="protokoll-punkt-text" rows="2" placeholder="Text / Vereinbarung" ${locked ? 'disabled' : ''}
|
||||
oninput="currentProtokollSetzePunktFeld(${idx}, 'text', this.value)">${esc(punkt.text)}</textarea>
|
||||
<div class="protokoll-punkt-foot">
|
||||
<label>Zuständig <input type="text" value="${esc(punkt.zustaendig)}" ${locked ? 'disabled' : ''}
|
||||
oninput="currentProtokollSetzePunktFeld(${idx}, 'zustaendig', this.value)"></label>
|
||||
<label>Termin
|
||||
<input type="text" id="protokollPunktTerminDisplay_${idx}" class="range-picker-display" value="${esc(terminDisplay)}" placeholder="—" 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)}">
|
||||
</label>
|
||||
<span class="protokoll-punkt-erstellt" title="Datum, an dem dieser Punkt zum ersten Mal erfasst wurde — bleibt beim Fortschreiben unverändert">Erstellt ${esc(formatDateDE(parseDateISO(punkt.erstelltAm)) || '—')}</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;
|
||||
};
|
||||
window.currentProtokollSetzePunktFeld = function(idx, feld, wert){
|
||||
if(!currentProtokoll || !currentProtokoll.punkte[idx]) return;
|
||||
currentProtokoll.punkte[idx][feld] = wert;
|
||||
};
|
||||
window.syncProtokollPunktTermin = function(idx){
|
||||
if(!currentProtokoll || !currentProtokoll.punkte[idx]) return;
|
||||
const el = document.getElementById('protokollPunktTermin_' + idx);
|
||||
if(el) currentProtokoll.punkte[idx].termin = el.value;
|
||||
};
|
||||
window.currentProtokollSetzePunktErledigt = function(idx, checked){
|
||||
if(!currentProtokoll || !currentProtokoll.punkte[idx]) return;
|
||||
currentProtokoll.punkte[idx].erledigt = checked;
|
||||
const row = document.getElementById('protokollPunkt_' + idx);
|
||||
if(row) row.classList.toggle('is-erledigt', checked);
|
||||
};
|
||||
// currentProtokollSetzePunktGruppe: einzige Punkt-Feldänderung, die neu
|
||||
// rendert (nur den Punkte-Block, siehe renderPunkteBlockHtml) — ändert
|
||||
// sich die Gruppe, muss der Punkt sichtbar in den passenden Abschnitt
|
||||
// wandern. Läuft über onchange (Blur/Enter), nicht oninput, damit während
|
||||
// des Tippens nicht bei jedem Zeichen neu gerendert wird.
|
||||
window.currentProtokollSetzePunktGruppe = function(idx, wert){
|
||||
if(!currentProtokoll || !currentProtokoll.punkte[idx]) return;
|
||||
currentProtokoll.punkte[idx].gruppe = wert;
|
||||
const block = document.getElementById('protokollPunkteBlock');
|
||||
if(block) block.innerHTML = renderPunkteBlockHtml();
|
||||
};
|
||||
|
||||
// ---------- Strukturelle Änderungen (Zeile hinzufügen/entfernen) ----------
|
||||
|
||||
window.fuegeTeilnehmerHinzu = function(){
|
||||
if(!currentProtokoll || app.isLocked()) return;
|
||||
currentProtokoll.teilnehmer.push({ name: '', firma: '', anwesend: true });
|
||||
renderProtokollDetail();
|
||||
};
|
||||
window.entferneTeilnehmer = function(idx){
|
||||
if(!currentProtokoll || app.isLocked()) return;
|
||||
currentProtokoll.teilnehmer.splice(idx, 1);
|
||||
renderProtokollDetail();
|
||||
};
|
||||
window.fuegePunktHinzu = function(gruppe){
|
||||
if(!currentProtokoll || app.isLocked()) 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).
|
||||
currentProtokoll.punkte.push({
|
||||
id: null, nummer: '', gruppe: gruppe || '', neu: true,
|
||||
thema: '', text: '', zustaendig: '', termin: '', erledigt: false,
|
||||
erstelltAm: currentProtokoll.datum || ''
|
||||
});
|
||||
const block = document.getElementById('protokollPunkteBlock');
|
||||
if(block) block.innerHTML = renderPunkteBlockHtml();
|
||||
};
|
||||
window.entfernePunkt = function(idx){
|
||||
if(!currentProtokoll || app.isLocked()) return;
|
||||
currentProtokoll.punkte.splice(idx, 1);
|
||||
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.isLocked()) 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);
|
||||
currentProtokoll.punkte.forEach((p, idx) => { p.nummer = nummern[idx]; });
|
||||
const body = {
|
||||
datum: currentProtokoll.datum,
|
||||
betreff: currentProtokoll.betreff,
|
||||
ort: currentProtokoll.ort,
|
||||
teilnehmer: currentProtokoll.teilnehmer.filter(t => String(t.name || '').trim()),
|
||||
punkte: currentProtokoll.punkte
|
||||
};
|
||||
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;
|
||||
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.isLocked()) 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));
|
||||
});
|
||||
};
|
||||
|
||||
// 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();
|
||||
};
|
||||
})();
|
||||
Reference in new issue
Block a user