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:
erikandClaude Opus 5.5 committed 2026-10-29 14:16:52 +01:00
1 parent f4fb3c550b
commit b7e0bd0d92
56 files changed
+12265 -2974

No files matched your search

+83 -21
View File
@@ -117,7 +117,7 @@ function renderBoardRow(p, allPeople, locked, weekLabel, opts){
const isOpen = !!boardRowOpen[p.id];
const tags = Array.isArray(p.tags) ? p.tags : [];
const tagsHtml = tags.map(t => `<span class="tag-chip">${esc(t)}</span>`).join('');
const previewText = statusPreviewText(p.status);
const previewText = statusPreviewText(kommentarVon(p, kommentarGewerk()).status);
const isNew = p.statusState === 'new';
// Avatar-Stapel der eingeklappten Zeile: nur tatsächlich zugeordnete
@@ -186,21 +186,47 @@ function renderBoardRow(p, allPeople, locked, weekLabel, opts){
// Wird bei jedem Aufbau der Schublade neu gesetzt.
let boardCommentBaseline = {};
// boardCommentUebernommen: je Projekt-ID, ob der gespeicherte Kommentar die
// unveränderte Übernahme aus der Vorwoche ist (Sitzungswechsel kopiert ihn,
// ensureCurrentWeekRollover() in server.js). Dann leert "Verwerfen" das Feld.
let boardCommentUebernommen = {};
// kommentarAusVorwocheUebernommen: true, wenn der Kommentar des gewählten
// Gewerks wortgleich und mit demselben Zeitstempel schon in einer früheren
// Sitzung steht — dort der jüngste nicht leere (die einmalige Übernahme der
// Einführung kam nicht immer aus der direkten Vorwoche).
// Parameter: p (Projekt), weekLabel (String). Rückgabe: Boolean.
function kommentarAusVorwocheUebernommen(p, weekLabel){
const g = kommentarGewerk();
if(!g || !p.projectUid) return false;
const jetzt = kommentarVon(p, g);
if(!(jetzt.status || '').trim()) return false;
const versionen = projectVersionsForUid(p.projectUid);
const idx = versionen.findIndex(v => v.week.label === weekLabel);
for(let i = (idx < 0 ? versionen.length : idx) - 1; i >= 0; i--){
const k = kommentarVon(versionen[i].project, g);
if(!(k.status || '').trim()) continue;
return k.status === jetzt.status && (k.statusAt || '') === (jetzt.statusAt || '');
}
return false;
}
// commentAuthorLine: Kopfzeile des Editors — wer den gespeicherten Kommentar
// verfasst hat und wann. Bestandskommentare ohne Verfasser (vor der
// Einführung von status_author) werden als "unbekannt" ausgewiesen, statt
// stillschweigend die aktuell angemeldete Person zu behaupten.
// Parameter: p (Projekt). Rückgabe: HTML-String.
function commentAuthorLine(p){
const hasText = statusPreviewText(p.status) !== '';
const k = kommentarVon(p, kommentarGewerk());
const hasText = statusPreviewText(k.status) !== '';
if(!hasText){
const me = (currentUser && currentUser.displayName) || '';
return me
? `<span class="comment-who"><span class="comment-av">${esc(initialsForName(me))}</span>${esc(me)}</span><span class="comment-sep">·</span><span>neuer Eintrag</span>`
: `<span>neuer Eintrag</span>`;
}
const author = p.statusAuthor || '';
const at = p.statusAt ? formatCommentStamp(p.statusAt) : '';
const author = k.statusAuthor || '';
const at = k.statusAt ? formatCommentStamp(k.statusAt) : '';
const who = author
? `<span class="comment-who"><span class="comment-av">${esc(initialsForName(author))}</span>${esc(author)}</span>`
: `<span class="comment-who comment-unknown">Verfasser unbekannt</span>`;
@@ -232,8 +258,10 @@ function formatCommentStamp(iso){
// Parameter: p (Projekt), weekLabel (String), locked (Boolean),
// dicht (Boolean). Rückgabe: HTML-String.
function renderCommentEditor(p, weekLabel, locked, dicht){
const html = sanitizeStatusHtml(p.status || '');
// Kommentar des gewählten Gewerks (Wochenbericht), siehe kommentarVon().
const html = sanitizeStatusHtml(kommentarVon(p, kommentarGewerk()).status || '');
boardCommentBaseline[p.id] = html;
boardCommentUebernommen[p.id] = kommentarAusVorwocheUebernommen(p, weekLabel);
if(locked){
return `<div class="comment-block" data-pid="${esc(p.id)}">
@@ -367,18 +395,17 @@ function saveBoardComment(el){
const p = currentWeek().projects.find(x => x.id === c.pid);
if(!p) return;
const before = (p.status || '').trim();
// Geschrieben wird immer der Kommentar EINES Gewerks — der Editor steht
// nur im Wochenbericht, und der hat stets ein gewähltes Gewerk.
const gewerk = kommentarGewerk();
if(!gewerk) return;
const before = (kommentarVon(p, gewerk).status || '').trim();
if(before === '' && html !== '') p.statusState = 'new';
else if(html === '') delete p.statusState;
p.status = html;
if(html === ''){
p.statusAuthor = '';
p.statusAt = '';
} else {
p.statusAuthor = (currentUser && currentUser.displayName) || '';
p.statusAt = new Date().toISOString();
}
kommentarSetzen(p, gewerk, html === ''
? { status: '', statusAuthor: '', statusAt: '' }
: { status: html, statusAuthor: (currentUser && currentUser.displayName) || '', statusAt: new Date().toISOString() });
boardCommentBaseline[c.pid] = html;
save();
renderBoard();
@@ -393,7 +420,9 @@ function saveBoardComment(el){
function discardBoardComment(el){
const c = commentParts(el);
if(!c || !c.field) return;
c.field.innerHTML = boardCommentBaseline[c.pid] || '';
// Übernommener Vorwochen-Kommentar: Verwerfen leert das ganze Feld, damit
// der Kommentar dieser Woche neu geschrieben werden kann (Eriks Wunsch).
c.field.innerHTML = boardCommentUebernommen[c.pid] ? '' : (boardCommentBaseline[c.pid] || '');
markCommentDirty(c.field);
richtextCursorAnsEnde(c.field);
}
@@ -408,6 +437,9 @@ function boardCommentKey(e, el){
} else if(e.key === 'Escape'){
discardBoardComment(el);
e.preventDefault();
} else {
// Enter: neue Zeile beginnt mit "- " (js/utils.js)
richtextNeueZeileMitStrich(e, el);
}
}
@@ -441,15 +473,31 @@ function takeCommentFromHistory(el, weekLabel){
const entry = projectStatusHistory(p.projectUid).find(e => e.week.label === weekLabel);
const html = entry
? entry.html
: (v => v ? sanitizeStatusHtml(v.project.status || '') : null)(
: (v => v ? sanitizeStatusHtml(kommentarVon(v.project, kommentarGewerk()).status || '') : null)(
projectVersionsForUid(p.projectUid).find(v => v.week.label === weekLabel));
if(html === null || html === undefined) return;
c.field.innerHTML = sanitizeStatusHtml(html);
kommentarVorsatzEntfernen(c.field);
markCommentDirty(c.field);
richtextCursorAnsEnde(c.field);
}
// kommentarVorsatzEntfernen: nimmt beim Übernehmen den Gewerke-Vorsatz am
// Anfang weg ("[ELT] ", "[ELT+HLS] " — Altbestand aus der Zeit vor den
// Kommentaren je Gewerk); im Kommentar eines Gewerks ist er überflüssig.
// Arbeitet auf dem ersten Textknoten, damit Formatierung erhalten bleibt.
// Parameter: feld (Element). Keine Rückgabe.
function kommentarVorsatzEntfernen(feld){
const walker = document.createTreeWalker(feld, NodeFilter.SHOW_TEXT);
let knoten;
while((knoten = walker.nextNode())){
if(!knoten.textContent.trim()) continue;
knoten.textContent = knoten.textContent.replace(/^\s*\[[^\]]{1,20}\]\s*/, '');
break;
}
}
// renderBoardDrawer: baut das aufgeklappte Detailpanel einer Projekt-Zeile —
// links der volle Status-Editor (unverändert gegenüber der früheren Matrix-
// Zelle) plus alle LPH-Balken, rechts die vollständige Rollen-/Personen-
@@ -574,7 +622,7 @@ function renderBoardDrawer(p, allPeople, locked, weekLabel, opts){
${!locked ? `<button type="button" class="plan-verweis" style="margin-top:4px" onclick="springZuEinstellungen('${p.id}')">In den Projekteinstellungen bearbeiten →</button>` : ''}
</div>
</div>
${typeof renderProjektVerlauf === 'function' ? renderProjektVerlauf(p, weekLabel, locked) : ''}
${(opts.ohneVerlauf || typeof renderProjektVerlauf !== 'function') ? '' : renderProjektVerlauf(p, weekLabel, locked)}
</div>`;
}
@@ -608,7 +656,7 @@ function renderBoardDrawer(p, allPeople, locked, weekLabel, opts){
// renderKommentarAnsicht: der Kommentar der laufenden Sitzung, nur zu lesen.
// Parameter: p (Projekt), locked. Rückgabe: HTML-String.
function renderKommentarAnsicht(p, locked){
const text = sanitizeStatusHtml(p.status || '');
const text = sanitizeStatusHtml(kommentarVon(p, kommentarGewerk()).status || '');
return `<div class="status-edit is-ansicht locked-cell">${text ||
'<span class="hint" style="margin:0">Für diese Sitzung ist noch kein Kommentar erfasst.</span>'}</div>`;
}
@@ -644,7 +692,8 @@ function renderBoard(){
if(container) container.innerHTML = '<div class="hint">Keine Sitzung geladen.</div>';
return;
}
const locked = isWeekLocked(w);
// Ziehen zwischen Gruppen ändert Stammdaten (Rollen & Rechte).
const locked = isWeekLocked(w) || !hatRecht('proj.stamm');
const grouped = peopleByRole(w);
const allPeople = [];
ROLES.forEach(r=> grouped[r].forEach(entry=>allPeople.push(entry)));
@@ -695,9 +744,22 @@ function renderBoard(){
function editLph(pid,lph,val){
if(guardWeekLocked()) return;
const p = currentWeek().projects.find(x=>x.id===pid);
val = val.trim();
if(!p) return;
if(!p.lph) p.lph = {};
val = String(val || '').trim();
if(val==="") delete p.lph[lph];
else p.lph[lph]=val.replace('%','');
else {
// Nur Zahlen von 0 bis 100 — "abc" oder "250" hätten Auslastung und
// Leistungsstand mit NaN bzw. über 100 % verrechnet. Ungültiges wird
// verworfen (Neuzeichnen stellt den alten Wert wieder her).
const n = Number(val.replace('%','').replace(',','.').trim());
if(!Number.isFinite(n)){
renderBoard();
if(typeof renderWochenbericht === 'function') renderWochenbericht();
return;
}
p.lph[lph] = String(Math.max(0, Math.min(100, Math.round(n))));
}
save(); renderBoard();
if(typeof renderWochenbericht === 'function') renderWochenbericht();
}