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

+136 -6
View File
@@ -70,9 +70,109 @@
// amtierende Leistungsphase, LPH-Prozentliste) aus app.project auf. Wird
// nach jedem Laden (renderPlanPage(), plan-state.js) sowie nach jeder
// Änderung hier selbst erneut aufgerufen. Keine Parameter, keine Rückgabe.
// ---------------------------------------------------------------------------
// Kommentar-Verlauf im Projekt-Dashboard
// ---------------------------------------------------------------------------
// planKommentarDaten: sucht über alle Sitzungen dieses Projekts (bis
// einschließlich der angezeigten) den jüngsten Kommentar und listet die
// früheren je Gewerk. Ein Eintrag im Verlauf entsteht nur, wenn sich der
// Text eines Gewerks gegenüber der Sitzung davor geändert hat — ein
// unverändert mitgeführter Kommentar stünde sonst jede Woche erneut da.
// Rückgabe: { letzter: {week, k, aktuell} | null, verlauf: [{week, gewerk, html, autor, at}] }
function planKommentarDaten(p, week){
const bisKey = week && week.label ? weekSortKey(week.label) : null;
let versionen = (typeof projectVersionsForUid === 'function' && p.projectUid)
? projectVersionsForUid(p.projectUid).filter(v => !bisKey || weekSortKey(v.week.label) <= bisKey)
: [];
// Angezeigte Sitzung immer mit dem lebenden Projektobjekt (Sandbox, oder
// ein Projekt, das in db.weeks noch nicht steht).
versionen = versionen.filter(v => !week || v.week.label !== week.label);
versionen.push({ week: week || { label: '' }, project: p });
let letzter = null;
for(let i = versionen.length - 1; i >= 0; i--){
const k = kommentarVon(versionen[i].project, null);
if(statusPlainText(k.status)){
letzter = { week: versionen[i].week, k, aktuell: i === versionen.length - 1 };
break;
}
}
const gewerke = (typeof DEPARTMENTS === 'object' && DEPARTMENTS) ? Object.keys(DEPARTMENTS) : ['ELT','HLS','ARCH'];
const verlauf = [];
gewerke.forEach(g => {
let vorher = '';
versionen.forEach(v => {
const k = kommentarVon(v.project, g);
const text = statusPlainText(k.status);
if(text && text !== vorher && !(letzter && v.week === letzter.week)){
verlauf.push({ week: v.week, gewerk: g, html: sanitizeStatusHtml(k.status), autor: k.statusAuthor || '', at: k.statusAt || '' });
}
vorher = text;
});
});
// Derselbe Text in mehreren Gewerken derselben Sitzung (Altbestand: die
// Umstellung auf Kommentare je Gewerk hat ihn in jedes kopiert) wird ein
// Eintrag mit allen Gewerken, statt zweimal untereinander zu stehen.
const zusammen = [];
verlauf.forEach(e => {
const gleich = zusammen.find(z => z.week === e.week && statusPlainText(z.html) === statusPlainText(e.html));
if(gleich){ gleich.gewerk += ', ' + e.gewerk; if(!gleich.autor) gleich.autor = e.autor; }
else zusammen.push(Object.assign({}, e));
});
zusammen.sort((a, b) => weekSortKey(b.week.label).localeCompare(weekSortKey(a.week.label))
|| String(b.at).localeCompare(String(a.at)));
return { letzter, verlauf: zusammen };
}
// "KW 43 / 2026" -> "KW 43" (das Jahr nur, wenn es nicht das laufende ist)
function kurzKw(label){
const m = /^(KW\s*\d+)\s*\/\s*(\d{4})$/.exec(String(label || '').trim());
if(!m) return label || '';
return m[2] === String(new Date().getFullYear()) ? m[1] : m[1] + ' / ' + m[2];
}
const PLAN_VERLAUF_KURZ = 3;
let planVerlaufAlle = false;
let planVerlaufProjekt = null;
function renderPlanKommentarVerlauf(verlauf){
const el = document.getElementById('planStatusVerlauf');
if(!el) return;
const uid = app.project && (app.project.projectUid || app.project.id);
if(uid !== planVerlaufProjekt){ planVerlaufProjekt = uid; planVerlaufAlle = false; }
if(!verlauf.length){ el.hidden = true; el.innerHTML = ''; return; }
el.hidden = false;
const zeigen = planVerlaufAlle ? verlauf : verlauf.slice(0, PLAN_VERLAUF_KURZ);
const zeilen = zeigen.map((e, i) => `
<li class="plan-verlauf-eintrag${i === 0 ? ' ist-juengster' : ''}">
<span class="plan-verlauf-kw">${esc(kurzKw(e.week.label))}</span>
<span class="plan-verlauf-punkt" aria-hidden="true"></span>
<div class="plan-verlauf-inhalt">
<div class="plan-verlauf-text">${e.html}</div>
<div class="plan-verlauf-meta">${esc(e.gewerk)}${e.autor ? ' · ' + esc(e.autor) : ''}</div>
</div>
</li>`).join('');
const mehr = verlauf.length > PLAN_VERLAUF_KURZ
? `<button type="button" class="plan-verlauf-mehr" onclick="planVerlaufUmschalten()">${planVerlaufAlle ? 'Weniger anzeigen' : 'Alle ' + verlauf.length + ' anzeigen'}</button>`
: '';
el.innerHTML = `
<div class="plan-verlauf-kopf">
<span class="plan-verlauf-titel">Verlauf</span>
<span class="plan-verlauf-anzahl">${verlauf.length === 1 ? '1 früherer Kommentar' : verlauf.length + ' frühere Kommentare'}</span>
</div>
<ol class="plan-verlauf-liste">${zeilen}</ol>${mehr}`;
}
window.planVerlaufUmschalten = function(){
planVerlaufAlle = !planVerlaufAlle;
window.renderPlanInfo();
};
window.renderPlanInfo = function(){
const p = app.project;
const locked = app.isLocked();
const locked = app.stammGesperrt();
// Kommentar, Fortschritt und Zuordnung sind hier NUR ZU SEHEN.
//
@@ -83,15 +183,42 @@
// Personen konnten dasselbe Feld an verschiedenen Stellen pflegen.
// Was hier bleibt, sind die Stammdaten des Projekts (Bereich
// "Einstellungen", siehe plan-settings.js).
// Gezeigt wird der ZULETZT GESCHRIEBENE Kommentar (Eriks Wunsch, Entwurf
// "A"): Hat die Sitzung noch keinen, der jüngste aus einer früheren — mit
// Hinweis, aus welcher. Darunter der Verlauf der früheren Kommentare.
const kom = planKommentarDaten(p, app.week);
const editEl = document.getElementById('planStatusEdit');
if(editEl){
editEl.className = 'status-edit is-ansicht';
editEl.contentEditable = 'false';
const text = sanitizeStatusHtml(p.status || '');
editEl.innerHTML = text || '<span class="hint" style="margin:0">Für diese Sitzung ist noch kein Kommentar erfasst.</span>';
// Alle Gewerke zusammen, je mit Gewerk davor (kommentarVon(), js/utils.js).
const text = kom.letzter ? sanitizeStatusHtml(kom.letzter.k.status) : '';
editEl.innerHTML = text || '<span class="hint" style="margin:0">Zu diesem Projekt ist noch kein Kommentar erfasst.</span>';
}
const hinweisEl = document.getElementById('planStatusHinweis');
if(hinweisEl){
const aelter = kom.letzter && !kom.letzter.aktuell;
hinweisEl.hidden = !aelter;
hinweisEl.innerHTML = aelter
? `In dieser Sitzung noch kein Kommentar — angezeigt wird der letzte aus <b>${esc(kurzKw(kom.letzter.week.label))}</b>.`
: '';
}
// Wer den Kommentar wann geschrieben hat (jüngster Eintrag aller Gewerke).
const autorEl = document.getElementById('planStatusAutor');
if(autorEl){
const k = kom.letzter ? kom.letzter.k : {};
autorEl.innerHTML = k.status && k.statusAuthor
? `<span class="plan-autor-ava">${esc(initialsForName(k.statusAuthor))}</span>${esc(k.statusAuthor)}`
+ (k.statusAt && typeof formatCommentStamp === 'function' ? ', ' + esc(formatCommentStamp(k.statusAt)) : '')
: '';
}
renderPlanKommentarVerlauf(kom.verlauf);
const statusWrap = document.getElementById('planStatusWrap');
if(statusWrap) statusWrap.classList.add('locked-cell');
// Nicht mehr als gesperrt markieren: .locked-cell dimmt auf 55 % und legt im
// Dunkelmodus ein Grau darunter — der Kommentar war dadurch kaum lesbar.
// Dass er hier nur angezeigt wird, zeigen schon .is-ansicht und der
// Verweis "Im Wochenbericht bearbeiten".
if(statusWrap) statusWrap.classList.remove('locked-cell');
setzeBerichtVerweis('planStatusVerweis', locked);
// Gruppe — Stammdatum, bleibt hier pflegbar
@@ -176,7 +303,7 @@
// setPlanGroup: ändert die Gruppe des Projekts. Parameter: val (String).
// Keine Rückgabe; bei gesperrter Woche abgebrochen.
window.setPlanGroup = function(val){
if(app.isLocked()) return;
if(app.stammGesperrt()) return;
app.project.group = val;
app.savePlan();
};
@@ -185,11 +312,14 @@
// Projekts auf. Parameter: val (LPH-Name oder leer). Rendert das
// Infopanel neu, damit die Markierung in der LPH-Liste sofort mitzieht.
window.setPlanActiveLp = function(val){
if(app.isLocked()) return;
if(app.stammGesperrt()) return;
if(val) app.project.aktuelleLP = [val];
else delete app.project.aktuelleLP;
app.savePlan();
window.renderPlanInfo();
// Abschnitt "Leistungsphasen" der Einstellungen: Kennzeichen "läuft"
// und die Kurzfassung in der Unternavigation nachziehen.
if(typeof renderPlanSettings === 'function') renderPlanSettings();
};
// Die Schreibfunktionen setPlanLph()/setPlanAssign()/planStatusBlur() sind