Termin aus Mail, Rollen & Rechte, Abwesenheiten, Glas-Darstellung und Pi-Betrieb
- Termin aus Mail: Mailtext oder .eml per Drag & Drop/Strg+V auf Terminplan oder Aufgaben, KI schlägt Aufgaben vor, Prüfung im Aufgaben-Panel; neues Notizfeld je Aufgabe (plan_todos.notiz) - Terminplan: Meilensteine ohne Aufgabenbezug je in eigener Zeile, "+ Neu"-Menü klappt neben schmaler Seitenleiste nach rechts, Leistungsphasen - Rollen & Rechte, Abwesenheiten-Seite, Benachrichtigungen, Rundgang - Glas-Darstellung, Logo als Favicon - Pi-Betrieb: PDF-Arbeit im Kindprozess, Wächter, Einrichtungs- und Prüfskript inkl. WLAN-Wächter Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f4fb3c550b
commit
b7e0bd0d92
56 files changed
+12265
-2974
No files matched your search
@@ -2,7 +2,7 @@
|
||||
// Projektplan — Protokolle (Besprechungsprotokolle je Projekt)
|
||||
// -----------------------------
|
||||
// Bereich "Protokolle" des Projektbereichs (siehe js/plan/plan-sections.js)
|
||||
// plus eine Kompakt-Karte im Dashboard (#planProtokolleSummary, springt per
|
||||
// plus eine Kachel im Dashboard (#planDashProtokolle, springt per
|
||||
// Klick über openProtokollePanel() in diesen Bereich — gleiches Muster wie
|
||||
// #planFilesSummary/openFilesPanel() in plan-files.js).
|
||||
//
|
||||
@@ -77,28 +77,103 @@
|
||||
});
|
||||
}
|
||||
|
||||
// renderProtokolleSummary: füllt die Kompakt-Vorschau der Dashboard-Karte
|
||||
// (#planProtokolleSummary) — letztes Protokoll + Anzahl offener Punkte,
|
||||
// gleiches Muster wie renderFilesSummary() in plan-files.js.
|
||||
// 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('planProtokolleSummary');
|
||||
const el = document.getElementById('planDashProtokolle');
|
||||
const panel = document.getElementById('planDashProtokollePanel');
|
||||
if(!el) return;
|
||||
if(!app.project || !app.project.nr){
|
||||
el.innerHTML = '<span class="hint">Keine Projektnummer.</span>';
|
||||
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 = '<span class="hint">Noch keine Protokolle.</span>';
|
||||
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 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>`;
|
||||
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(){
|
||||
@@ -175,7 +250,7 @@
|
||||
// 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;
|
||||
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);
|
||||
@@ -197,7 +272,7 @@
|
||||
// Parameter: gruppe (Name), richtung (-1 = nach vorn, +1 = nach hinten).
|
||||
// Keine Rückgabe.
|
||||
window.verschiebeGruppe = function(gruppe, richtung){
|
||||
if(!currentProtokoll || app.isLocked()) return;
|
||||
if(!currentProtokoll || app.protokolleGesperrt()) return;
|
||||
const punkte = currentProtokoll.punkte;
|
||||
const namen = [];
|
||||
punkte.forEach(p => {
|
||||
@@ -230,7 +305,7 @@
|
||||
function renderProtokolleListe(){
|
||||
const container = document.getElementById('protokolleContainer');
|
||||
if(!container) return;
|
||||
const locked = app.isLocked();
|
||||
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){
|
||||
@@ -262,7 +337,7 @@
|
||||
// "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;
|
||||
if(app.protokolleGesperrt()) return;
|
||||
let teilnehmer = [];
|
||||
try {
|
||||
if(protokolleCache.length > 0){
|
||||
@@ -317,7 +392,7 @@
|
||||
// firmenweiten Stand, und kann sich seit dem letzten Öffnen geändert
|
||||
// haben.
|
||||
window.oeffneProtokollImportDialog = function(){
|
||||
if(app.isLocked()) return;
|
||||
if(app.protokolleGesperrt()) return;
|
||||
document.getElementById('protokollImportFile').value = '';
|
||||
document.getElementById('protokollImportBetreff').value = '';
|
||||
document.getElementById('protokollImportOrt').value = '';
|
||||
@@ -460,7 +535,7 @@
|
||||
const container = document.getElementById('protokolleContainer');
|
||||
if(!container || !currentProtokoll) return;
|
||||
const p = currentProtokoll;
|
||||
const locked = app.isLocked();
|
||||
const locked = app.protokolleGesperrt();
|
||||
|
||||
const datumDisplay = formatDateDE(parseDateISO(p.datum)) || '';
|
||||
const quelleHinweis = p.quelleDateiname
|
||||
@@ -634,7 +709,7 @@
|
||||
// aktualisiereListenZeile() oben.
|
||||
function renderPunkteBlockHtml(){
|
||||
const p = currentProtokoll;
|
||||
const locked = app.isLocked();
|
||||
const locked = app.protokolleGesperrt();
|
||||
const gruppen = gruppenReihenfolge();
|
||||
const nummern = window.berechneProtokollNummern(p.punkte);
|
||||
|
||||
@@ -717,7 +792,7 @@
|
||||
// in currentProtokoll.punkte, -1 = kein Punkt vorhanden), nummern (das
|
||||
// Ergebnis von berechneProtokollNummern()). Rückgabe: HTML-String.
|
||||
function renderSchreibbereichHtml(idx, nummern){
|
||||
const locked = app.isLocked();
|
||||
const locked = app.protokolleGesperrt();
|
||||
if(idx < 0){
|
||||
return `<div class="protokoll-schreiben ist-leer">
|
||||
<p>Noch kein Punkt angelegt.</p>
|
||||
@@ -874,17 +949,17 @@
|
||||
// ---------- Strukturelle Änderungen (Zeile hinzufügen/entfernen) ----------
|
||||
|
||||
window.fuegeTeilnehmerHinzu = function(){
|
||||
if(!currentProtokoll || app.isLocked()) return;
|
||||
if(!currentProtokoll || app.protokolleGesperrt()) return;
|
||||
currentProtokoll.teilnehmer.push({ name: '', firma: '', anwesend: true });
|
||||
renderProtokollDetail();
|
||||
};
|
||||
window.entferneTeilnehmer = function(idx){
|
||||
if(!currentProtokoll || app.isLocked()) return;
|
||||
if(!currentProtokoll || app.protokolleGesperrt()) return;
|
||||
currentProtokoll.teilnehmer.splice(idx, 1);
|
||||
renderProtokollDetail();
|
||||
};
|
||||
window.fuegePunktHinzu = function(gruppe){
|
||||
if(!currentProtokoll || app.isLocked()) return;
|
||||
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
|
||||
@@ -894,7 +969,19 @@
|
||||
thema: '', text: '', zustaendig: '', termin: '', status: 'offen',
|
||||
erstelltAm: currentProtokoll.datum || '', entfaelltNaechstes: false
|
||||
};
|
||||
currentProtokoll.punkte.push(neuerPunkt);
|
||||
// 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.
|
||||
@@ -914,7 +1001,7 @@
|
||||
// (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.isLocked()) return;
|
||||
if(!currentProtokoll || app.protokolleGesperrt()) return;
|
||||
const bestehende = new Set(gruppenReihenfolge());
|
||||
let name = 'Neue Gruppe';
|
||||
let n = 2;
|
||||
@@ -927,7 +1014,7 @@
|
||||
if(input){ input.focus(); input.select(); }
|
||||
};
|
||||
window.entfernePunkt = function(idx){
|
||||
if(!currentProtokoll || app.isLocked()) return;
|
||||
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.
|
||||
@@ -947,7 +1034,7 @@
|
||||
// Punkte komplett (siehe server.js). Ausdrückliches Speichern, kein
|
||||
// Auto-Save, wie die Board-Kommentare.
|
||||
window.speichereProtokoll = function(){
|
||||
if(!currentProtokoll || app.isLocked()) return;
|
||||
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 —
|
||||
@@ -995,7 +1082,7 @@
|
||||
};
|
||||
|
||||
window.loescheProtokoll = function(){
|
||||
if(!currentProtokoll || app.isLocked()) return;
|
||||
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(); })
|
||||
|
||||
Reference in new issue
Block a user