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
+83
-21
@@ -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();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user