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
@@ -56,7 +56,11 @@ function lphSignature(p){
|
||||
// ihr (z. B. gerade erst angelegtes Projekt) gilt es als neu. Parameter: p
|
||||
// (Projekt der aktuellen Sitzung), prevWeek (Vorwoche-Objekt oder null).
|
||||
// Rückgabe: { bucket: 'new'|'status'|'progress'|'unchanged', reason: String }.
|
||||
function classifyProjectChange(p, prevWeek){
|
||||
function classifyProjectChange(p, prevWeek, gewerk){
|
||||
// Kommentare werden je Gewerk verglichen (kommentarVon(), js/utils.js) —
|
||||
// ein neuer ELT-Kommentar ist für die HLS-Besprechung keine Neuigkeit.
|
||||
// Ohne ausdrückliches Gewerk gilt das des Zusammenhangs.
|
||||
const g = gewerk === undefined ? kommentarGewerk() : gewerk;
|
||||
const prev = (prevWeek && p.projectUid)
|
||||
? prevWeek.projects.find(x => x.projectUid === p.projectUid)
|
||||
: null;
|
||||
@@ -64,8 +68,10 @@ function classifyProjectChange(p, prevWeek){
|
||||
if(!prev){
|
||||
return { bucket: 'new', reason: 'Neues Projekt' };
|
||||
}
|
||||
const statusNow = (p.status || '').trim();
|
||||
const statusPrev = (prev.status || '').trim();
|
||||
const kNow = kommentarVon(p, g);
|
||||
const kPrev = kommentarVon(prev, g);
|
||||
const statusNow = (kNow.status || '').trim();
|
||||
const statusPrev = (kPrev.status || '').trim();
|
||||
// Nur ein tatsächlich vorhandener, geänderter Text zählt als neuer
|
||||
// Kommentar. Seit cloneWeek() (js/weeks.js) den Text der Vorwoche nicht
|
||||
// mehr mitkopiert, startet jede duplizierte Sitzung mit leerem Kommentar —
|
||||
@@ -81,7 +87,7 @@ function classifyProjectChange(p, prevWeek){
|
||||
// wortgleich den Text der Vorwoche, setzt dabei aber einen neuen
|
||||
// Zeitstempel. Ohne diese Prüfung landete das Projekt unter "Unverändert",
|
||||
// obwohl diese Woche ausdrücklich etwas dazu festgehalten wurde.
|
||||
if(statusNow !== '' && p.statusAt && p.statusAt !== (prev.statusAt || '')){
|
||||
if(statusNow !== '' && kNow.statusAt && kNow.statusAt !== (kPrev.statusAt || '')){
|
||||
return { bucket: 'status', reason: 'Kommentar bestätigt' };
|
||||
}
|
||||
if(lphSignature(p) !== lphSignature(prev)){
|
||||
@@ -166,9 +172,12 @@ function renderAttendance(){
|
||||
const w = currentWeek();
|
||||
if(!w){ container.innerHTML = ''; return; }
|
||||
|
||||
const locked = isWeekLocked(w);
|
||||
// Ohne Recht „Anwesenheit erfassen“ (Rollen & Rechte) nur Anzeige.
|
||||
const locked = isWeekLocked(w) || !hatRecht('wb.anwesenheit');
|
||||
const att = w.attendance || {};
|
||||
const people = Array.isArray(w.people) ? w.people : [];
|
||||
// Nur die Personen des gewählten Gewerks — jedes Gewerk erfasst die
|
||||
// Anwesenheit seiner eigenen Besprechung (personenDesGewerks(), js/utils.js).
|
||||
const people = personenDesGewerks(w, wochenberichtGewerk);
|
||||
// Fokus-Modus (Eriks Wahl "C"): die Anwesenheit ist ein aufklappbarer
|
||||
// Abschnitt wie Leistungsphasen/Beteiligte, kein dauerhaft sichtbarer Block
|
||||
// mehr — Zustand in berichtDisclosureState (js/board/board.js).
|
||||
@@ -177,7 +186,7 @@ function renderAttendance(){
|
||||
if(people.length === 0){
|
||||
container.innerHTML = `<details class="bericht-disclosure" data-bericht-abschnitt="anwesenheit" ${offen}>
|
||||
<summary><span>Anwesenheit</span><span class="chev">${icon('chevronRight')}</span></summary>
|
||||
<div class="bericht-disclosure-body"><span class="hint" style="margin:0">Für diese Sitzung sind keine Projektbeteiligten hinterlegt.</span></div>
|
||||
<div class="bericht-disclosure-body"><span class="hint" style="margin:0">Für dieses Gewerk sind in dieser Sitzung keine Projektbeteiligten hinterlegt.</span></div>
|
||||
</details>`;
|
||||
return;
|
||||
}
|
||||
@@ -213,7 +222,7 @@ function renderAttendance(){
|
||||
}).join('');
|
||||
|
||||
container.innerHTML = `<details class="bericht-disclosure" data-bericht-abschnitt="anwesenheit" ${offen}>
|
||||
<summary><span>Anwesenheit der Sitzung</span><span class="count">${anwesend} von ${people.length}</span><span class="chev">${icon('chevronRight')}</span></summary>
|
||||
<summary><span>Anwesenheit ${esc(wochenberichtGewerk || "")}</span><span class="count">${anwesend} von ${people.length}</span><span class="chev">${icon('chevronRight')}</span></summary>
|
||||
<div class="bericht-disclosure-body">${actions}<div class="attendance-grid">${tiles}</div></div>
|
||||
</details>`;
|
||||
}
|
||||
@@ -243,7 +252,7 @@ function setAllAttendance(present){
|
||||
const w = currentWeek();
|
||||
if(!w) return;
|
||||
if(!w.attendance) w.attendance = {};
|
||||
(w.people || []).forEach(p => { w.attendance[p.id] = !!present; });
|
||||
personenDesGewerks(w, wochenberichtGewerk).forEach(p => { w.attendance[p.id] = !!present; });
|
||||
save();
|
||||
renderAttendance();
|
||||
}
|
||||
@@ -352,8 +361,7 @@ function aktuellesBerichtProjekt(w){
|
||||
// Parameter: uid (project_uid). Keine Rückgabe.
|
||||
function waehleBerichtProjekt(uid){
|
||||
berichtAuswahl = uid;
|
||||
vorwocheRadIndex = 0; // neues Projekt beginnt wieder bei der jüngsten Vorwoche
|
||||
vorwocheRadScrollAkku = 0; // kein halb aufgelaufenes Scrollen vom vorherigen Projekt übernehmen
|
||||
berichtVerlaufAlle = false; // neues Projekt zeigt wieder nur die jüngsten Einträge
|
||||
renderWochenbericht();
|
||||
const detail = document.getElementById('berichtDetail');
|
||||
if(detail) detail.scrollIntoView({ block: 'nearest' });
|
||||
@@ -374,23 +382,39 @@ function waehleBerichtProjekt(uid){
|
||||
// das noch offen ist. Der Motor des Meetings — ohne ihn sucht man nach jedem
|
||||
// Projekt von Hand die nächste Zeile. Parameter: pid. Keine Rückgabe.
|
||||
function besprochenUndWeiter(pid){
|
||||
if(guardWeekLocked()) return;
|
||||
if(guardWeekLocked() || !hatRecht('wb.besprochen')) return;
|
||||
const w = currentWeek();
|
||||
const p = (w.projects || []).find(x => x.id === pid);
|
||||
if(!p) return;
|
||||
p.besprochen = true;
|
||||
kommentarSetzen(p, wochenberichtGewerk, { besprochen: true });
|
||||
save();
|
||||
|
||||
const liste = berichtProjekte(w);
|
||||
const idx = liste.findIndex(x => x.id === pid);
|
||||
// Erst hinter der aktuellen Stelle suchen, dann von vorn — so landet man
|
||||
// auch dann beim nächsten offenen Punkt, wenn man mittendrin eingestiegen ist.
|
||||
const offen = liste.slice(idx + 1).find(x => !x.besprochen)
|
||||
|| liste.find(x => !x.besprochen);
|
||||
const istAb = x => kommentarVon(x, wochenberichtGewerk).besprochen;
|
||||
const offen = liste.slice(idx + 1).find(x => !istAb(x))
|
||||
|| liste.find(x => !istAb(x));
|
||||
berichtAuswahl = offen ? offen.projectUid : p.projectUid;
|
||||
renderWochenbericht();
|
||||
}
|
||||
|
||||
// berichtNaechstes: das Projekt direkt nach pid in der sichtbaren Liste
|
||||
// (dieselbe Reihenfolge und Suche wie die Pfeiltaste ↓), sonst null.
|
||||
function berichtNaechstes(w, pid){
|
||||
const liste = berichtProjekte(w).filter(passtZurSuche);
|
||||
const idx = liste.findIndex(x => x.id === pid);
|
||||
return idx >= 0 ? (liste[idx + 1] || null) : null;
|
||||
}
|
||||
|
||||
// berichtWeiter: springt zum nächsten Projekt, OHNE es abzuhaken — für ein
|
||||
// Projekt, das heute nicht besprochen wird. Parameter: pid. Keine Rückgabe.
|
||||
function berichtWeiter(pid){
|
||||
const naechstes = berichtNaechstes(currentWeek(), pid);
|
||||
if(naechstes) waehleBerichtProjekt(naechstes.projectUid || null);
|
||||
}
|
||||
|
||||
// setzeAlleBesprochen: setzt alle Haken der Sitzung zurück bzw. hakt alles ab.
|
||||
// Parameter: wert (Boolean). Keine Rückgabe.
|
||||
function setzeAlleBesprochen(wert){
|
||||
@@ -402,7 +426,9 @@ function setzeAlleBesprochen(wert){
|
||||
// ANDEREN Gewerke als besprochen, die in dieser Besprechung nie zu sehen
|
||||
// waren (widerspricht "Wochenbericht — Gewerke-Trennung": Vollständigkeit
|
||||
// gilt innerhalb des gewählten Gewerks, nicht über alle hinweg).
|
||||
berichtProjekte(w).forEach(p => { p.besprochen = !!wert; });
|
||||
// Nur der Haken DIESES Gewerks — die anderen Gewerke besprechen das
|
||||
// Projekt in ihrer eigenen Sitzung.
|
||||
berichtProjekte(w).forEach(p => kommentarSetzen(p, wochenberichtGewerk, { besprochen: !!wert }));
|
||||
save();
|
||||
renderWochenbericht();
|
||||
}
|
||||
@@ -410,20 +436,34 @@ function setzeAlleBesprochen(wert){
|
||||
// ---------- Abwesenheit ----------
|
||||
|
||||
// abwesendeBeteiligte: die einem Projekt zugeordneten Personen, die heute
|
||||
// fehlen. "Heute" ist der Anwesenheitsstand dieser Sitzung (w.attendance) —
|
||||
// er wird am Fuß des Berichts ohnehin erfasst. Punkte, deren Zuständige
|
||||
// fehlen, vertagt man im Meeting besser.
|
||||
// Parameter: p (Projekt), w (Woche). Rückgabe: Array von Personennamen.
|
||||
// laut ihrer EINGETRAGENEN Abwesenheit fehlen (Urlaub, Krank, Fortbildung …
|
||||
// aus der Benutzerverwaltung, db.globalPeople[].absences) — Eriks Wunsch.
|
||||
// Früher wurde stattdessen die Anwesenheitsliste am Fuß des Berichts
|
||||
// ausgewertet: Wer dort nicht abgehakt war, "fehlte" — auch wenn er nur
|
||||
// nicht in dieser Besprechung saß, und umgekehrt fehlte ein Urlauber nicht,
|
||||
// solange niemand die Anwesenheit erfasst hatte. Punkte, deren Zuständige
|
||||
// abwesend sind, vertagt man im Meeting besser.
|
||||
// Zugeordnet = in irgendeiner Rolle (Haupt oder Unterstützung) im Projekt.
|
||||
// Die Person der Sitzung wird über ihre Kennung, ersatzweise den vollen
|
||||
// Namen, ihrem Konto zugeordnet (wie personGewerk(), js/utils.js).
|
||||
// Parameter: p (Projekt), w (Woche). Rückgabe: Array von Texten
|
||||
// "Name (Urlaub bis 28.10.)".
|
||||
function abwesendeBeteiligte(p, w){
|
||||
const att = w.attendance || {};
|
||||
// Solange niemand als anwesend markiert ist, ist die Anwesenheit schlicht
|
||||
// noch nicht erfasst — dann wäre "alle fehlen" eine Falschmeldung.
|
||||
if(!Object.values(att).some(v => v)) return [];
|
||||
const heute = heuteIso();
|
||||
const konten = Array.isArray(db.globalPeople) ? db.globalPeople : [];
|
||||
const namen = [];
|
||||
(w.people || []).forEach(person => {
|
||||
const rollen = Array.isArray(person.role) ? person.role : [person.role];
|
||||
const zugeordnet = rollen.some(r => (p.assign || {})[assignKey(person.id, r)] > 0);
|
||||
if(zugeordnet && !att[person.id] && namen.indexOf(person.name) === -1) namen.push(person.name);
|
||||
if(!zugeordnet) return;
|
||||
const konto = konten.find(x => x.id === person.id)
|
||||
|| (person.fullName ? konten.find(x => x.fullName === person.fullName) : null);
|
||||
const a = konto && (konto.absences || []).find(x => x.start && x.end && x.start <= heute && heute <= x.end);
|
||||
if(!a) return;
|
||||
const name = person.fullName || person.name;
|
||||
const bis = a.end > heute ? ' bis ' + formatWeekDate(a.end).replace(/\d{4}$/, '') : '';
|
||||
const text = name + ' (' + (a.type || 'abwesend') + bis + ')';
|
||||
if(!namen.some(n => n.startsWith(name + ' ('))) namen.push(text);
|
||||
});
|
||||
return namen;
|
||||
}
|
||||
@@ -487,13 +527,13 @@ function renderWochenbericht(){
|
||||
if(locked){
|
||||
html += `<div class="lock-notice">
|
||||
Diese Sitzung ist gesperrt. Bearbeiten nur nach Entsperren möglich.
|
||||
<button type="button" class="secondary small" onclick="unlockCurrentWeek()">Entsperren</button>
|
||||
${hatRecht('s.entsperren') ? '<button type="button" class="secondary small" onclick="unlockCurrentWeek()">Entsperren</button>' : ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if(liste.length === 0){
|
||||
html += renderBerichtKopf(w, liste, prevWeek, locked);
|
||||
html += `<div class="hint">In dieser Sitzung ist noch kein Projekt angelegt.</div>`;
|
||||
html += `<div class="hint">Für ${esc(wochenberichtGewerk)} steht in dieser Sitzung kein Projekt auf der Tagesordnung.</div>`;
|
||||
container.innerHTML = html;
|
||||
renderAttendance();
|
||||
return;
|
||||
@@ -560,7 +600,7 @@ function renderBerichtKopfNeu(){
|
||||
// renderBerichtKopf: Gewerke-Wahl, Fortschrittsring und das Sprungfeld.
|
||||
// Parameter: w, liste, prevWeek, locked. Rückgabe: HTML.
|
||||
function renderBerichtKopf(w, liste, prevWeek, locked){
|
||||
const ab = liste.filter(p => p.besprochen).length;
|
||||
const ab = liste.filter(p => kommentarVon(p, wochenberichtGewerk).besprochen).length;
|
||||
const pct = liste.length ? Math.round(ab / liste.length * 100) : 0;
|
||||
// Der Vergleichs-Hinweis ("Vergleich mit KW …") steht nicht mehr als eigene
|
||||
// Zeile da, sondern als Titel-Tooltip am Fortschritt — dieselbe Angabe,
|
||||
@@ -569,7 +609,7 @@ function renderBerichtKopf(w, liste, prevWeek, locked){
|
||||
? `Vergleich mit ${esc(prevWeek.label)}${prevWeek.date ? ' (' + esc(formatWeekDate(prevWeek.date)) + ')' : ''}.`
|
||||
: 'Dies ist die früheste vorhandene Sitzung — kein Vergleich zur Vorwoche möglich.';
|
||||
|
||||
const resetBtn = locked ? '' : (ab > 0
|
||||
const resetBtn = (locked || !hatRecht('wb.besprochen')) ? '' : (ab > 0
|
||||
? `<button type="button" class="bericht-reset-link" onclick="setzeAlleBesprochen(false)">Zurücksetzen</button>`
|
||||
: `<button type="button" class="bericht-reset-link" onclick="setzeAlleBesprochen(true)">Alle abhaken</button>`);
|
||||
|
||||
@@ -627,7 +667,7 @@ function renderBerichtListe(){
|
||||
const gruppenkopf = gruppe !== gruppeVorher ? `<div class="bericht-rail-gruppe">${esc(gruppe)}</div>` : '';
|
||||
gruppeVorher = gruppe;
|
||||
const name = (p.nr ? p.nr + ' · ' : '') + p.name;
|
||||
return `${gruppenkopf}<div class="bericht-rail-item${istAktiv ? ' is-aktiv' : ''}${p.besprochen ? ' is-ab' : ''}"
|
||||
return `${gruppenkopf}<div class="bericht-rail-item${istAktiv ? ' is-aktiv' : ''}${kommentarVon(p, wochenberichtGewerk).besprochen ? ' is-ab' : ''}"
|
||||
role="button" tabindex="0" data-uid="${esc(p.projectUid || '')}"
|
||||
aria-current="${istAktiv ? 'true' : 'false'}">
|
||||
<span class="bericht-rail-num">${String(position).padStart(2, '0')}</span>
|
||||
@@ -651,6 +691,9 @@ function renderBerichtListe(){
|
||||
// "Besprochen & weiter". Parameter: p, w, prevWeek, locked. Rückgabe: HTML.
|
||||
function renderBerichtDetail(p, w, prevWeek, locked){
|
||||
if(!p) return '';
|
||||
// Kommentar und Fortschritt nur mit Recht „Kommentar und Fortschritt
|
||||
// bearbeiten“, sonst wie gesperrt als Anzeige (Rollen & Rechte).
|
||||
const kLocked = locked || !hatRecht('wb.kommentar');
|
||||
const grouped = peopleByRole(w);
|
||||
const allPeople = [];
|
||||
ROLES.forEach(r => grouped[r].forEach(entry => allPeople.push(entry)));
|
||||
@@ -669,89 +712,179 @@ function renderBerichtDetail(p, w, prevWeek, locked){
|
||||
</div>
|
||||
<h1 class="bericht-h1">${esc(p.name)}</h1>
|
||||
<div class="bericht-sub">${subText}</div>
|
||||
${fehlen.length ? `<span class="bericht-abwesend-hinweis">Fehlt heute: ${esc(fehlen.join(', '))} — eventuell vertagen.</span>` : ''}
|
||||
${renderVorwocheRad(p, w)}
|
||||
${renderBoardDrawer(p, allPeople, locked, w.label, BERICHT_OPTS)}
|
||||
${locked ? '' : `<div class="bericht-detail-fuss">
|
||||
<button type="button" class="secondary small" onclick="openTodoAusKommentar('${esc(p.id)}')">
|
||||
${fehlen.length ? `<span class="bericht-abwesend-hinweis">Heute abwesend: ${esc(fehlen.join(', '))} — eventuell vertagen.</span>` : ''}
|
||||
${renderBoardDrawer(p, allPeople, kLocked, w.label, Object.assign({}, BERICHT_OPTS, { ohneVerlauf: true }))}
|
||||
<div class="bericht-detail-fuss">
|
||||
${!locked && hatRecht('proj.plan') ? `<button type="button" class="secondary small" onclick="openTodoAusKommentar('${esc(p.id)}')">
|
||||
Aufgabe aus Kommentar…
|
||||
</button>
|
||||
</button>` : ''}
|
||||
<span class="bericht-fuss-spacer"></span>
|
||||
<button type="button" onclick="besprochenUndWeiter('${esc(p.id)}')">Besprochen & weiter →</button>
|
||||
</div>`}`;
|
||||
<button type="button" class="secondary" onclick="berichtWeiter('${esc(p.id)}')"${berichtNaechstes(w, p.id) ? '' : ' disabled title="Letztes Projekt der Liste"'}>Weiter →</button>
|
||||
${!locked && hatRecht('wb.besprochen') ? `<button type="button" onclick="besprochenUndWeiter('${esc(p.id)}')">Besprochen & weiter →</button>` : ''}
|
||||
</div>
|
||||
${renderBerichtVerlauf(p, w, kLocked)}`;
|
||||
}
|
||||
|
||||
// vorwocheRadIndex: Position im Kommentar-Verlauf des GERADE GEÖFFNETEN
|
||||
// Projekts (0 = jüngste Vorwoche, höher = weiter zurück). Ein einzelner
|
||||
// modulweiter Stand genügt, da immer nur ein Projekt im Fokus steht;
|
||||
// waehleBerichtProjekt() setzt ihn beim Projektwechsel zurück.
|
||||
let vorwocheRadIndex = 0;
|
||||
// ---------- Verlauf der Vorwochen (Eriks Wahl "B") ----------
|
||||
//
|
||||
// Löst das Scrollrad über dem Kommentarfeld (renderVorwocheRad()) und den
|
||||
// aufklappbaren Kachel-Streifen (renderProjektVerlauf(), bleibt für
|
||||
// Projektliste/Projektbereich) im Wochenbericht ab. Darstellung wie im
|
||||
// Projekt-Dashboard (renderPlanKommentarVerlauf(), plan-info.js): eine
|
||||
// senkrechte Zeitleiste, hier als eigener Abschnitt über die volle Breite
|
||||
// unter der Bearbeitungsmaske. Über dem Kommentarfeld bleibt nur die schmale
|
||||
// Zeile "Zuletzt KW …" (renderBerichtZuletzt()).
|
||||
//
|
||||
// Ein Eintrag entsteht nur, wenn sich der Kommentar DES GEWÄHLTEN GEWERKS
|
||||
// gegenüber der Sitzung davor geändert hat; Sitzungen dazwischen ohne neuen
|
||||
// Text stehen als grauer Vermerk am Eintrag. Geänderte Leistungsphasen seit
|
||||
// dem vorigen Eintrag erscheinen als Chips ("LPH 5: 60 → 75 %"), statt in
|
||||
// jedem Eintrag alle Werte zu wiederholen. Keine neue Datenhaltung — jede
|
||||
// Sitzung hält ihre eigene Kopie des Projekts (projectVersionsForUid()).
|
||||
|
||||
// renderVorwocheRad: löst renderVorwochenzeile() ab, die nur die
|
||||
// UNMITTELBARE Vorwoche zeigte. Eriks Rückmeldung zum Entwurfsvergleich: mit
|
||||
// dem Mausrad (oder den beiden Pfeilen) durch MEHRERE vergangene Kommentare
|
||||
// blättern können, nicht nur eine Vorwoche sehen. Nutzt
|
||||
// projectStatusHistory() (js/utils.js) — denselben, bereits für den
|
||||
// Verlaufs-Dialog geprüften Verlauf (nur Sitzungen mit tatsächlich
|
||||
// geändertem Text, neueste zuerst), abzüglich der aktuellen Sitzung selbst
|
||||
// (die steht ja bereits im Eingabefeld darunter). Parameter: p, w (aktuelle
|
||||
// Sitzung). Rückgabe: HTML (leer ohne Verlauf).
|
||||
function renderVorwocheRad(p, w){
|
||||
const BERICHT_VERLAUF_KURZ = 3;
|
||||
// berichtVerlaufAlle: "Alle N anzeigen" aufgeklappt; waehleBerichtProjekt()
|
||||
// setzt es beim Projektwechsel zurück.
|
||||
let berichtVerlaufAlle = false;
|
||||
|
||||
// berichtKurzKw: "KW 43 / 2026" -> "KW 43" (Jahr nur, wenn nicht das laufende)
|
||||
function berichtKurzKw(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];
|
||||
}
|
||||
|
||||
// berichtVerlaufDaten: die früheren Kommentare eines Projekts für ein Gewerk.
|
||||
// Parameter: p (Projekt der Sitzung w), w (aktuelle Sitzung), g (Gewerk oder
|
||||
// null). Rückgabe: { sitzungen, eintraege: [{week, html, autor, at, ruhe, chips}] },
|
||||
// neueste zuerst.
|
||||
function berichtVerlaufDaten(p, w, g){
|
||||
const bis = weekSortKey(w.label);
|
||||
const versionen = projectVersionsForUid(p.projectUid)
|
||||
.filter(v => weekSortKey(v.week.label).localeCompare(bis) < 0);
|
||||
const roh = [];
|
||||
let vorher = '';
|
||||
versionen.forEach((v, i) => {
|
||||
const k = kommentarVon(v.project, g);
|
||||
const text = statusPlainText(k.status);
|
||||
if(text && text !== vorher){
|
||||
roh.push({ i, week: v.week, project: v.project, html: sanitizeStatusHtml(k.status || ''),
|
||||
autor: k.statusAuthor || '', at: k.statusAt || '' });
|
||||
}
|
||||
vorher = text;
|
||||
});
|
||||
const eintraege = roh.map((e, n) => {
|
||||
// Sitzungen nach diesem Eintrag bis zum nächsten (bzw. bis zur aktuellen)
|
||||
const bisIdx = n + 1 < roh.length ? roh[n + 1].i : versionen.length;
|
||||
const still = versionen.slice(e.i + 1, bisIdx).map(v => berichtKurzKw(v.week.label));
|
||||
const ruhe = still.length === 0 ? ''
|
||||
: (still.length === 1 ? still[0] : still[0] + ' bis ' + still[still.length - 1]) + ' ohne neuen Kommentar';
|
||||
// LPH-Änderungen gegenüber dem vorigen Eintrag (beim ältesten gegenüber
|
||||
// der Sitzung direkt davor, sofern es eine gibt)
|
||||
const basis = n > 0 ? roh[n - 1].project : (e.i > 0 ? versionen[e.i - 1].project : null);
|
||||
const chips = [];
|
||||
if(basis){
|
||||
const alt = basis.lph || {}, neu = e.project.lph || {};
|
||||
LPHS.forEach(l => {
|
||||
const a = alt[l] === undefined || alt[l] === '' ? '' : String(alt[l]);
|
||||
const b = neu[l] === undefined || neu[l] === '' ? '' : String(neu[l]);
|
||||
if(a !== b && b !== '') chips.push(l + ': ' + (a || '0') + ' → ' + b + ' %');
|
||||
});
|
||||
}
|
||||
return { week: e.week, html: e.html, autor: e.autor, at: e.at, ruhe, chips };
|
||||
}).reverse();
|
||||
return { sitzungen: versionen.length, eintraege };
|
||||
}
|
||||
|
||||
// berichtVerlaufDatum: ISO-Zeitstempel -> "20.10." (leer, wenn unbrauchbar)
|
||||
function berichtVerlaufDatum(at){
|
||||
const d = at ? new Date(at) : null;
|
||||
if(!d || isNaN(d)) return '';
|
||||
return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' });
|
||||
}
|
||||
|
||||
// renderBerichtZuletzt: NICHT MEHR AUFGERUFEN (Eriks Wunsch — der Kommentar
|
||||
// der Vorwoche steht seit der Übernahme beim Sitzungswechsel ohnehin im
|
||||
// Feld). Früher: die schmale Zeile über dem Kommentarfeld — der
|
||||
// jüngste frühere Kommentar dieses Gewerks. Rückgabe: HTML (leer ohne Verlauf).
|
||||
function renderBerichtZuletzt(p, w){
|
||||
if(!p.projectUid) return '';
|
||||
const verlauf = projectStatusHistory(p.projectUid).filter(e => e.week.label !== w.label);
|
||||
if(verlauf.length === 0) return '';
|
||||
const i = Math.max(0, Math.min(verlauf.length - 1, vorwocheRadIndex));
|
||||
const eintrag = verlauf[i];
|
||||
const punkte = verlauf.map((_, idx) => `<span class="dot${idx === i ? ' aktiv' : ''}"></span>`).join('');
|
||||
return `<div class="bericht-vorwoche-rad" title="Mit dem Mausrad durch den Verlauf blättern" onwheel="vorwocheRadScroll(event)">
|
||||
<button type="button" class="bericht-vorwoche-nav" onclick="vorwocheRadSchritt(1)" ${i >= verlauf.length - 1 ? 'disabled' : ''} title="Weiter zurück">‹</button>
|
||||
<div class="bericht-vorwoche-inhalt">
|
||||
<span class="bericht-vorwoche-label">${esc(eintrag.week.label)}</span>
|
||||
<span class="bericht-vorwoche-text">${esc(eintrag.text)}</span>
|
||||
</div>
|
||||
<button type="button" class="bericht-vorwoche-nav" onclick="vorwocheRadSchritt(-1)" ${i <= 0 ? 'disabled' : ''} title="Neuer">›</button>
|
||||
<div class="bericht-vorwoche-dots">${punkte}</div>
|
||||
const e = berichtVerlaufDaten(p, w, wochenberichtGewerk).eintraege[0];
|
||||
if(!e) return '';
|
||||
// Mit Formatierung (fett, kursiv, Farbe, Zeilen) wie im Verlauf darunter —
|
||||
// e.html ist bereits über sanitizeStatusHtml() bereinigt.
|
||||
return `<div class="bericht-zuletzt">
|
||||
<span class="bericht-zuletzt-kw">Zuletzt ${esc(berichtKurzKw(e.week.label))}</span>
|
||||
<div class="bericht-zuletzt-text">${e.html}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// vorwocheRadSchritt: bewegt vorwocheRadIndex um delta (geklemmt auf den
|
||||
// vorhandenen Verlauf) und zeichnet den Bericht neu — derselbe vollständige
|
||||
// Neuaufbau, den auch editLph() auslöst; ein offener
|
||||
// Kommentar-Editor übersteht das über dasselbe Fokus-Wiederherstellungsnetz
|
||||
// (pendingCommentFocus, js/board/board.js), eine aufgeklappte Anwesenheit
|
||||
// über berichtDisclosureState. Parameter: delta (1 = weiter zurück, -1 =
|
||||
// neuer). Keine Rückgabe.
|
||||
function vorwocheRadSchritt(delta){
|
||||
vorwocheRadScrollAkku = 0; // ein gezielter Klick verwirft ein halb aufgelaufenes Scrollen
|
||||
const w = currentWeek();
|
||||
const p = aktuellesBerichtProjekt(w);
|
||||
if(!w || !p) return;
|
||||
const max = projectStatusHistory(p.projectUid).filter(e => e.week.label !== w.label).length - 1;
|
||||
vorwocheRadIndex = Math.max(0, Math.min(max, vorwocheRadIndex + delta));
|
||||
renderWochenbericht();
|
||||
// renderBerichtVerlauf: der Abschnitt "Verlauf der Vorwochen" unter der
|
||||
// Bearbeitungsmaske. Parameter: p, w, locked. Rückgabe: HTML.
|
||||
function renderBerichtVerlauf(p, w, locked){
|
||||
if(!p || !p.projectUid) return '';
|
||||
const g = wochenberichtGewerk;
|
||||
const { sitzungen, eintraege } = berichtVerlaufDaten(p, w, g);
|
||||
const gText = g || 'alle Gewerke';
|
||||
let inhalt;
|
||||
if(eintraege.length === 0){
|
||||
inhalt = `<div class="hint" style="margin:0">${sitzungen === 0
|
||||
? 'Dieses Projekt erscheint zum ersten Mal, es gibt noch keine frühere Sitzung.'
|
||||
: 'In den früheren Sitzungen gibt es noch keinen Kommentar für ' + esc(gText) + '.'}</div>`;
|
||||
} else {
|
||||
const zeigen = berichtVerlaufAlle ? eintraege : eintraege.slice(0, BERICHT_VERLAUF_KURZ);
|
||||
inhalt = `<ol class="bv-liste">${zeigen.map((e, i) => {
|
||||
const datum = berichtVerlaufDatum(e.at);
|
||||
const wer = [e.autor, datum].filter(Boolean).join(', ');
|
||||
// onmousedown + preventDefault wie beim bisherigen "übernehmen": sonst
|
||||
// verliert das Kommentarfeld zuerst den Fokus und speichert dazwischen.
|
||||
const knopf = locked ? '<span></span>' :
|
||||
`<button type="button" class="bv-uebernehmen"
|
||||
onmousedown="event.preventDefault();berichtVerlaufUebernehmen('${esc(e.week.label)}')"
|
||||
title="Diesen Kommentar ins Kommentarfeld übernehmen">übernehmen</button>`;
|
||||
return `<li class="bv-zeile${i === 0 ? ' ist-juengster' : ''}">
|
||||
<span class="bv-kw"><b>${esc(berichtKurzKw(e.week.label))}</b>${wer ? `<span>${esc(wer)}</span>` : ''}</span>
|
||||
<span class="bv-punkt" aria-hidden="true"></span>
|
||||
<span class="bv-inhalt">
|
||||
<span class="bv-text">${e.html}</span>
|
||||
${e.ruhe ? `<span class="bv-ruhe">${esc(e.ruhe)}</span>` : ''}
|
||||
</span>
|
||||
<span class="bv-chips">${e.chips.map(c => `<span class="bv-chip">${esc(c)}</span>`).join('')}</span>
|
||||
${knopf}
|
||||
</li>`;
|
||||
}).join('')}</ol>`;
|
||||
}
|
||||
const mehr = eintraege.length > BERICHT_VERLAUF_KURZ
|
||||
? `<button type="button" class="bv-mehr" onclick="berichtVerlaufUmschalten()">${berichtVerlaufAlle ? 'Weniger anzeigen' : 'Alle ' + eintraege.length + ' anzeigen'}</button>`
|
||||
: '';
|
||||
const anzahl = eintraege.length === 1 ? '1 Kommentar' : eintraege.length + ' Kommentare';
|
||||
return `<section class="bericht-verlauf" id="berichtVerlauf" aria-label="Verlauf der Vorwochen">
|
||||
<div class="bv-kopf">
|
||||
<h2 class="bv-titel">Verlauf der Vorwochen</h2>
|
||||
<span class="bv-anzahl">${esc(gText)}, ${anzahl} in ${sitzungen === 1 ? '1 Sitzung' : sitzungen + ' Sitzungen'}</span>
|
||||
<span class="bericht-fuss-spacer"></span>
|
||||
${mehr}
|
||||
</div>
|
||||
${inhalt}
|
||||
</section>`;
|
||||
}
|
||||
|
||||
// vorwocheRadScrollAkku/-SCHWELLE: jeder einzelne wheel-Tick löste zunächst
|
||||
// sofort einen Schritt aus — auf einem Trackpad erzeugt schon ein kurzes
|
||||
// Antippen mehrere Ticks mit kleinem deltaY, wodurch ein einziges Scrollen
|
||||
// mehrere Wochen auf einmal übersprang (Erik: "Empfindlichkeit beim Scrollen
|
||||
// etwas verlangsamen"). Jetzt wird deltaY aufsummiert, bis die Schwelle
|
||||
// überschritten ist — eine einzelne Mausrad-Rastung (meist ~100) überschreitet
|
||||
// sie sofort und bleibt damit bei einem Schritt je Rastung, ein kurzes
|
||||
// Trackpad-Antippen dagegen oft nicht. Erster Richtwert, bei Bedarf
|
||||
// nachjustieren.
|
||||
let vorwocheRadScrollAkku = 0;
|
||||
const VORWOCHE_RAD_SCHWELLE = 50;
|
||||
// berichtVerlaufUmschalten: "Alle N anzeigen" / "Weniger anzeigen". Zeichnet
|
||||
// nur den Verlauf neu, damit ein angefangener Kommentar unberührt bleibt.
|
||||
function berichtVerlaufUmschalten(){
|
||||
berichtVerlaufAlle = !berichtVerlaufAlle;
|
||||
const w = currentWeek();
|
||||
const p = w && aktuellesBerichtProjekt(w);
|
||||
const el = document.getElementById('berichtVerlauf');
|
||||
if(!p || !el) return;
|
||||
el.outerHTML = renderBerichtVerlauf(p, w, isWeekLocked(w));
|
||||
}
|
||||
|
||||
// vorwocheRadScroll: Mausrad-/Trackpad-Bedienung desselben Scrollrads —
|
||||
// runter (deltaY positiv, aufsummiert) blättert weiter zurück, hoch blättert
|
||||
// zu neueren Einträgen. e.preventDefault() verhindert, dass dieselbe Bewegung
|
||||
// zusätzlich die Seite scrollt. Parameter: e (WheelEvent). Keine Rückgabe.
|
||||
function vorwocheRadScroll(e){
|
||||
e.preventDefault();
|
||||
vorwocheRadScrollAkku += e.deltaY;
|
||||
if(Math.abs(vorwocheRadScrollAkku) < VORWOCHE_RAD_SCHWELLE) return;
|
||||
vorwocheRadSchritt(vorwocheRadScrollAkku > 0 ? 1 : -1);
|
||||
// berichtVerlaufUebernehmen: schreibt den Kommentar jener Sitzung in das
|
||||
// Kommentarfeld der aktuellen (takeCommentFromHistory(), js/board/board.js).
|
||||
function berichtVerlaufUebernehmen(weekLabel){
|
||||
const feld = document.querySelector('#berichtDetail .comment-block .comment-field');
|
||||
if(feld) takeCommentFromHistory(feld, weekLabel);
|
||||
}
|
||||
|
||||
// ---------- Aufgabe aus dem Kommentar ----------
|
||||
@@ -783,7 +916,7 @@ function openTodoAusKommentar(pid){
|
||||
// daraus reinen Text mit erhaltenen Zeilenumbrüchen, aus dem unten die
|
||||
// erste Zeile als Titelvorschlag gezogen wird.
|
||||
const feld = document.querySelector('#wochenbericht .comment-field');
|
||||
const text = feld ? statusTextMitZeilen(feld.innerHTML) : statusPlainText(p.status);
|
||||
const text = feld ? statusTextMitZeilen(feld.innerHTML) : statusPlainText(kommentarVon(p, wochenberichtGewerk).status);
|
||||
const ersteZeile = String(text || '').split('\n').map(z => z.replace(/^[-•*\s]+/, '').trim()).find(z => z) || '';
|
||||
|
||||
document.getElementById('todoKommentarTitel').value = ersteZeile.slice(0, 120);
|
||||
@@ -832,6 +965,13 @@ function saveTodoAusKommentar(){
|
||||
return;
|
||||
}
|
||||
const ende = document.getElementById('todoKommentarEnde').value;
|
||||
// Fälligkeit vor heute ergäbe einen Balken mit negativer Breite (Start ist
|
||||
// immer heute) — lieber gleich ablehnen.
|
||||
if(ende && ende < formatDateISO(new Date())){
|
||||
fehler.textContent = 'Das Fälligkeitsdatum liegt in der Vergangenheit.';
|
||||
fehler.hidden = false;
|
||||
return;
|
||||
}
|
||||
const personen = Array.from(document.querySelectorAll('#todoKommentarPersonen input:checked')).map(i => i.value);
|
||||
|
||||
if(!p.plan) p.plan = { start:'', end:'', todos:[], milestones:[], people:[], progress:0 };
|
||||
@@ -870,8 +1010,9 @@ function openBerichtProtokoll(){
|
||||
if(prevWeek) zeilen.push('Vergleich mit ' + prevWeek.label);
|
||||
zeilen.push('');
|
||||
|
||||
const anwesend = (w.people || []).filter(x => (w.attendance || {})[x.id]).map(x => x.fullName || x.name);
|
||||
const fehlend = (w.people || []).filter(x => !(w.attendance || {})[x.id]).map(x => x.fullName || x.name);
|
||||
const leute = personenDesGewerks(w, wochenberichtGewerk);
|
||||
const anwesend = leute.filter(x => (w.attendance || {})[x.id]).map(x => x.fullName || x.name);
|
||||
const fehlend = leute.filter(x => !(w.attendance || {})[x.id]).map(x => x.fullName || x.name);
|
||||
if(anwesend.length){
|
||||
zeilen.push('Anwesend: ' + anwesend.join(', '));
|
||||
if(fehlend.length) zeilen.push('Entschuldigt: ' + fehlend.join(', '));
|
||||
@@ -884,7 +1025,7 @@ function openBerichtProtokoll(){
|
||||
if(info.bucket === 'unchanged') return; // nur, was sich geändert hat
|
||||
anzahl++;
|
||||
zeilen.push('— ' + (p.nr ? p.nr + ' ' : '') + p.name + ' [' + info.reason + ']');
|
||||
const text = statusPlainText(p.status);
|
||||
const text = statusPlainText(kommentarVon(p, wochenberichtGewerk).status);
|
||||
if(text) zeilen.push(' ' + text);
|
||||
// Aufgaben, die in dieser Sitzung entstanden sind, gehören ins Protokoll:
|
||||
// Sie sind das Ergebnis der Besprechung.
|
||||
|
||||
Reference in new issue
Block a user