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
+232
-207
@@ -29,7 +29,7 @@
|
||||
// betroffene Projekt. Jede Kachel/jedes Ereignis ist eine reine Projektion
|
||||
// bereits geladener Daten (dasselbe db aus GET /load-all, das jeder andere
|
||||
// Tab auch nutzt) — keine eigene Server-Route, keine schreibenden Aktionen.
|
||||
// Wiederverwendet werden berechneAuslastung()/loadAmpelColor()/
|
||||
// Wiederverwendet werden aufgabenVerlaufJePerson()/loadAmpelColor()/
|
||||
// kapazitaetsFaktor()/stundenFormat()/lpActiveList()/schwelleGelb()/
|
||||
// schwelleRot() aus js/load/load.js,
|
||||
// classifyProjectChange()/findPreviousWeek()/springZumWochenbericht() aus
|
||||
@@ -78,7 +78,6 @@
|
||||
const WAND_GEWERKE = ['ELT', 'HLS', 'ARCH'];
|
||||
|
||||
const WAND_REFRESH_MS = 60000; // Hintergrund-Aktualisierung
|
||||
const WAND_FEED_MAX = 6; // Höchstzahl Einträge im "Aktuell"-Panel
|
||||
// WAND_IDLE_MS: Erik hat einen echten Touch-Monitor — er darf davortreten und
|
||||
// in der Projektliste unterhalb der Kacheln scrollen ("ich kann mich vor die
|
||||
// Tafel stellen und scrollen"). Nach dieser Zeit ohne jede Berührung kehrt
|
||||
@@ -192,9 +191,30 @@ function wandKehreZurueck(){
|
||||
// überrascht zu werden. Läuft über ein eigenes 1-Sekunden-Intervall statt
|
||||
// über wandIdleTimer selbst, da der nur EIN MAL (beim Auslösen) feuert.
|
||||
// Keine Parameter, keine Rückgabe.
|
||||
// wandImGrundzustand: ganz oben gescrollt und keine Übersicht offen — dann
|
||||
// gibt es nichts zurückzusetzen, und der Rücksprung-Zähler ruht (Eriks
|
||||
// Wunsch: "Der Rücksprung-Timer muss nur laufen, wenn die Seite nicht nach
|
||||
// ganz oben gescrollt ist"). Rückgabe: Boolean.
|
||||
function wandImGrundzustand(){
|
||||
const c = document.getElementById('wandContent');
|
||||
return !wandUebersicht && (!c || c.scrollTop <= 2);
|
||||
}
|
||||
|
||||
function wandCountdownTick(){
|
||||
const uhr = document.getElementById('wtUhr');
|
||||
if(uhr) uhr.textContent = new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
||||
const el = document.getElementById('wandIdleCountdown');
|
||||
if(!el) return;
|
||||
// Im Grundzustand ruht die Uhr: ausblenden und die volle Minute
|
||||
// vorhalten — sobald jemand scrollt oder eine Übersicht öffnet, beginnt
|
||||
// sie dort bei 60 s (das Scrollen meldet wandAktivitaetErkannt() ohnehin).
|
||||
if(document.body.classList.contains('wandtafel-aktiv') && wandImGrundzustand()){
|
||||
clearTimeout(wandIdleTimer);
|
||||
wandIdleTimer = setTimeout(wandBeiUntaetigkeitZuruecksetzen, WAND_IDLE_MS);
|
||||
wandIdleDeadline = Date.now() + WAND_IDLE_MS;
|
||||
el.textContent = '';
|
||||
return;
|
||||
}
|
||||
const rest = Math.max(0, Math.ceil((wandIdleDeadline - Date.now()) / 1000));
|
||||
el.textContent = rest > 0 ? ('Rücksprung in ' + rest + ' s') : '';
|
||||
}
|
||||
@@ -209,6 +229,7 @@ function wandCountdownTick(){
|
||||
// laufen, nicht nach dem ersten Rücksprung stehen bleiben, solange niemand
|
||||
// die Tafel berührt. Keine Parameter, keine Rückgabe.
|
||||
function wandBeiUntaetigkeitZuruecksetzen(){
|
||||
if(wandImGrundzustand()){ wandAktivitaetErkannt(); return; }
|
||||
wandUebersicht = null;
|
||||
renderWandboard({ resetScroll: true });
|
||||
wandAktivitaetErkannt();
|
||||
@@ -305,17 +326,18 @@ function renderWandboard(opts){
|
||||
}
|
||||
|
||||
container.classList.add('wand-scrollable');
|
||||
container.innerHTML = '<div class="wand-ambient">'
|
||||
+ wandDashKopfHtml(w)
|
||||
+ wandKpiReiheHtml(w)
|
||||
+ '<div class="dash-grid">'
|
||||
+ wandPanel('Auslastung / Gewerk', wandAuslastungInhalt(w))
|
||||
+ wandPanel('Besprochen / Gewerk', wandBesprochenInhalt(w))
|
||||
+ wandPanel('Aktuell', wandAktuellInhalt(w))
|
||||
+ '</div>'
|
||||
// Entwurf "Wandtafel in Glas" (Eriks Wahl): Kopf → vier Kennzahlen →
|
||||
// links die Gewerke (Auslastung + Besprochen je Zeile), rechts die
|
||||
// nächsten fünf Arbeitstage → Hinweis auf die Projektliste darunter.
|
||||
container.innerHTML = '<div class="wand-ambient wt">'
|
||||
+ wtKopfHtml(w)
|
||||
+ wtKpiHtml(w)
|
||||
+ '<div class="wt-felder">' + wtGewerkeHtml(w) + wtTageHtml(w) + '</div>'
|
||||
+ wtFussHtml(w)
|
||||
+ '</div>'
|
||||
+ wandProjektlisteHtml(w);
|
||||
container.scrollTop = vorherigerScroll;
|
||||
wtSpaltenEinpassen();
|
||||
}
|
||||
|
||||
// wandGesamtKennzahlen: zwei über alle Gewerke hinweg zusammengefasste
|
||||
@@ -342,61 +364,6 @@ function wandGesamtKennzahlen(w){
|
||||
};
|
||||
}
|
||||
|
||||
// wandDashKopfHtml: Kopfzeile der Normalansicht — Marke "Dashboard" links,
|
||||
// rechts wer heute abwesend ist (antippbar, öffnet dieselbe Abwesenheiten-
|
||||
// Übersicht wie zuvor die Heute-Kachel) und Sitzung/Uhrzeit als laufendes
|
||||
// Lebenszeichen (ersetzt das frühere separate "Stand: HH:MM Uhr" am Fuß der
|
||||
// rechten Spalte — ein Uhrzeit-Hinweis genügt, jetzt prominent oben statt
|
||||
// versteckt unten). Parameter: w. Rückgabe: HTML-String.
|
||||
function wandDashKopfHtml(w){
|
||||
const d = wandHeuteDaten(w);
|
||||
const uhrzeit = new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
||||
const abwesendHtml = d.abwesend.length
|
||||
? '<div class="dash-abwesend-mini ist-klickbar" onclick="wandZeigeUebersicht(\'abwesend\')" title="Abwesend heute">'
|
||||
+ d.abwesend.map(p => '<span class="dash-avatar-mini">' + esc(initialsFor(p.name)) + '</span>').join('')
|
||||
+ '</div>'
|
||||
: '';
|
||||
return '<div class="dash-kopf"><span class="dash-marke">Dashboard</span>'
|
||||
+ '<div class="dash-kopf-rechts">' + abwesendHtml
|
||||
+ '<span class="dash-datum">' + esc(w.label) + ' — ' + esc(uhrzeit) + ' Uhr</span>'
|
||||
+ '</div></div>';
|
||||
}
|
||||
|
||||
// wandKpiReiheHtml: die vier Kennzahlen-Kacheln oben — Auslastung gesamt,
|
||||
// Besprochen gesamt (wandGesamtKennzahlen()), offene Aufgaben und
|
||||
// überfällig (wandHeuteDaten(), dieselben Zahlen, die zuvor die Heute-Kachel
|
||||
// zeigte). Die beiden letzten bleiben wie zuvor antippbar und öffnen
|
||||
// dieselbe Übersicht. Parameter: w. Rückgabe: HTML-String.
|
||||
function wandKpiReiheHtml(w){
|
||||
const g = wandGesamtKennzahlen(w);
|
||||
const heute = wandHeuteDaten(w);
|
||||
|
||||
const auslastungTxt = g.auslastung === null ? '—' : Math.round(g.auslastung * 100) + '%';
|
||||
const auslastungKlasse = g.auslastung === null ? ''
|
||||
: (g.auslastung >= schwelleRot() ? 'crit' : g.auslastung >= schwelleGelb() ? 'warn' : 'ok');
|
||||
const besprochenTxt = g.besprochen === null ? '—' : Math.round(g.besprochen * 100) + '%';
|
||||
const ueberfaelligKlasse = heute.ueberfaelligeListe.length > 0 ? 'crit' : 'ok';
|
||||
|
||||
return '<div class="dash-kpi-reihe">'
|
||||
+ '<div class="dash-kpi"><div class="dash-kpi-label">Auslastung ges.</div>'
|
||||
+ '<div class="dash-kpi-wert ' + auslastungKlasse + '">' + auslastungTxt + '</div></div>'
|
||||
+ '<div class="dash-kpi"><div class="dash-kpi-label">Besprochen</div>'
|
||||
+ '<div class="dash-kpi-wert info">' + besprochenTxt + '</div></div>'
|
||||
+ '<div class="dash-kpi ist-klickbar" onclick="wandZeigeUebersicht(\'offen\')" title="Alle offenen Aufgaben anzeigen">'
|
||||
+ '<div class="dash-kpi-label">Offene Aufgaben</div><div class="dash-kpi-wert ok">' + heute.offen + '</div></div>'
|
||||
+ '<div class="dash-kpi ist-klickbar" onclick="wandZeigeUebersicht(\'ueberfaellig\')" title="Alle überfälligen Aufgaben anzeigen">'
|
||||
+ '<div class="dash-kpi-label">Überfällig</div><div class="dash-kpi-wert ' + ueberfaelligKlasse + '">' + heute.ueberfaelligeListe.length + '</div></div>'
|
||||
+ '</div>';
|
||||
}
|
||||
|
||||
// wandPanel: die gemeinsame, nicht-interaktive Panel-Umrahmung der
|
||||
// Drei-Panel-Reihe (Auslastung/Besprochen/Aktuell). Parameter: titel,
|
||||
// inhalt (HTML). Rückgabe: HTML-String.
|
||||
function wandPanel(titel, inhalt){
|
||||
return '<div class="dash-panel"><div class="dash-panel-titel">' + esc(titel) + '</div>'
|
||||
+ '<div class="dash-panel-body">' + inhalt + '</div></div>';
|
||||
}
|
||||
|
||||
// wandProjektlisteHtml: die vollständige Projektliste unterhalb der
|
||||
// Kennzahl-Kacheln — auf Eriks ausdrücklichen Wunsch ("Die Projektliste soll
|
||||
// so aussehen wie die Originale") keine eigene, vereinfachte Darstellung,
|
||||
@@ -552,16 +519,10 @@ function wandAuslastungJeGewerk(w){
|
||||
if(gewerke.length === 0) return;
|
||||
gewerke.forEach(g => { ergebnis[g].gesamt++; });
|
||||
|
||||
const aktivePhase = lpActiveList(pr)[0];
|
||||
const probe = berechneAuslastung(null, pr, aktivePhase, ROLES[0], 1, null);
|
||||
if(!probe.unbewertbar) gewerke.forEach(g => { ergebnis[g].bewertbar++; });
|
||||
|
||||
let projektStunden = 0;
|
||||
Object.keys(pr.assign || {}).forEach(aKey => {
|
||||
const rolle = aKey.slice(aKey.indexOf('|') + 1);
|
||||
const al = berechneAuslastung(null, pr, aktivePhase, rolle, pr.assign[aKey], pr.assign);
|
||||
if(al.stunden > 0) projektStunden += al.stunden;
|
||||
});
|
||||
// Stunden dieser Woche allein aus Aufgaben mit Aufwand (load.js).
|
||||
const av = aufgabenVerlaufJePerson(pr);
|
||||
if(offeneAufwandsAufgaben(pr).length) gewerke.forEach(g => { ergebnis[g].bewertbar++; });
|
||||
const projektStunden = Object.values(av).reduce((s, v) => s + (v[0] || 0), 0);
|
||||
gewerke.forEach(g => {
|
||||
ergebnis[g].stunden += projektStunden;
|
||||
if(projektStunden > 0) ergebnis[g].projekte.push({ id: pr.id, name: pr.name, nr: pr.nr, stunden: projektStunden });
|
||||
@@ -571,35 +532,6 @@ function wandAuslastungJeGewerk(w){
|
||||
return ergebnis;
|
||||
}
|
||||
|
||||
// wandAuslastungInhalt: ein Meter je Gewerk, Ampelfarbe wie im
|
||||
// Auslastungs-Tab — der Inhalt des Panels "Auslastung / Gewerk"
|
||||
// (wandPanel() rahmt ihn). Jede Zeile ist einzeln antippbar und öffnet die
|
||||
// Projektliste dieses Gewerks (wandZeigeUebersicht('auslastung', g)). Eine
|
||||
// Null bekommt KEINE grüne Farbe (dieselbe Regel wie in renderLoad()) —
|
||||
// "keine Daten" statt eines irreführend guten Balkens. Parameter: w.
|
||||
// Rückgabe: HTML-String.
|
||||
function wandAuslastungInhalt(w){
|
||||
const daten = wandAuslastungJeGewerk(w);
|
||||
return WAND_GEWERKE.map(g => {
|
||||
const d = daten[g];
|
||||
const label = (DEPARTMENTS[g] && DEPARTMENTS[g].label) || g;
|
||||
const klick = ' onclick="wandZeigeUebersicht(\'auslastung\',\'' + g + '\')"';
|
||||
if(d.bewertbar === 0 || d.kapazitaet <= 0){
|
||||
return '<div class="wand-meter-zeile ist-klickbar"' + klick + '>'
|
||||
+ '<span class="wand-meter-label">' + esc(label) + '</span>'
|
||||
+ '<div class="wand-meter"><span class="wand-meter-leer">keine Daten</span></div>'
|
||||
+ '<span class="wand-meter-wert">—</span></div>';
|
||||
}
|
||||
const quote = d.stunden / d.kapazitaet;
|
||||
const ampel = loadAmpelColor(quote);
|
||||
const breite = Math.min(100, quote * 100);
|
||||
return '<div class="wand-meter-zeile ist-klickbar"' + klick + '>'
|
||||
+ '<span class="wand-meter-label">' + esc(label) + '</span>'
|
||||
+ '<div class="wand-meter"><div class="wand-meter-fill" style="width:' + breite + '%;background:' + ampel + '"></div></div>'
|
||||
+ '<span class="wand-meter-wert" style="color:' + ampel + '">' + stundenFormat(d.stunden) + '<span class="wand-einheit">h</span></span></div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// wandUebersichtAuslastung: alle Projekte, die zu einem Gewerk beitragen,
|
||||
// nach Stunden absteigend — Antippen springt ins Projekt (Abschnitt
|
||||
// "Leistung", der dort ohnehin schon errechnete Honorar/Fortschritt-Bezug
|
||||
@@ -631,7 +563,8 @@ function wandBesprochenDaten(w){
|
||||
WAND_GEWERKE.forEach(g => {
|
||||
const relevante = (w.projects || []).filter(p =>
|
||||
Array.isArray(p.gewerke) && p.gewerke.includes(g) && p.group !== 'In Anbahnung');
|
||||
const offeneProjekte = relevante.filter(p => !p.besprochen);
|
||||
// Je Gewerk sein eigener Haken (kommentarVon(), js/utils.js).
|
||||
const offeneProjekte = relevante.filter(p => !kommentarVon(p, g).besprochen);
|
||||
ergebnis[g] = {
|
||||
besprochen: relevante.length - offeneProjekte.length,
|
||||
gesamt: relevante.length,
|
||||
@@ -641,31 +574,6 @@ function wandBesprochenDaten(w){
|
||||
return ergebnis;
|
||||
}
|
||||
|
||||
// wandBesprochenInhalt: ein Meter je Gewerk ("X von Y") — der Inhalt des
|
||||
// Panels "Besprochen / Gewerk" (wandPanel() rahmt ihn). Jede Zeile ist
|
||||
// einzeln antippbar und öffnet die Liste der noch offenen Projekte dieses
|
||||
// Gewerks (wandZeigeUebersicht('besprochen', g)). Die Füllfarbe nutzt das
|
||||
// dunklere Logo-Blau (--dash-ink-soft) statt des früheren generischen
|
||||
// App-Akzents, damit sie zur Wandtafel-eigenen Farbwelt passt (styles.css).
|
||||
// Parameter: w. Rückgabe: HTML-String.
|
||||
function wandBesprochenInhalt(w){
|
||||
const daten = wandBesprochenDaten(w);
|
||||
return WAND_GEWERKE.map(g => {
|
||||
const d = daten[g];
|
||||
const label = (DEPARTMENTS[g] && DEPARTMENTS[g].label) || g;
|
||||
if(d.gesamt === 0){
|
||||
return '<div class="wand-meter-zeile"><span class="wand-meter-label">' + esc(label) + '</span>'
|
||||
+ '<span class="wand-meter-leer">keine Projekte</span></div>';
|
||||
}
|
||||
const quote = d.besprochen / d.gesamt;
|
||||
const klick = ' onclick="wandZeigeUebersicht(\'besprochen\',\'' + g + '\')"';
|
||||
return '<div class="wand-meter-zeile ist-klickbar"' + klick + '>'
|
||||
+ '<span class="wand-meter-label">' + esc(label) + '</span>'
|
||||
+ '<div class="wand-meter"><div class="wand-meter-fill" style="width:' + (quote * 100) + '%;background:var(--dash-ink-soft)"></div></div>'
|
||||
+ '<span class="wand-meter-wert">' + d.besprochen + ' von ' + d.gesamt + '</span></div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// wandUebersichtBesprochen: die noch nicht besprochenen Projekte eines
|
||||
// Gewerks — Antippen springt NICHT in den Projektbereich, sondern in den
|
||||
// Wochenbericht (springZumWochenbericht(), js/plan/plan-info.js): Der
|
||||
@@ -780,87 +688,204 @@ function wandUebersichtAbwesend(w){
|
||||
|
||||
// ---------- Ereignis-Feed (Panel "Aktuell") ----------
|
||||
|
||||
// wandAktuelleEreignisse: sammelt drei Ereignisarten aus allen Projekten der
|
||||
// Sitzung — neue/aktualisierte Kommentare (classifyProjectChange()), frisch
|
||||
// überfällige Aufgaben und anstehende Meilensteine — als EINE, nach
|
||||
// zeitlicher Nähe zu heute sortierte Liste (|Tage bis Datum| aufsteigend,
|
||||
// nicht nach Datum selbst: Ein Ereignis von gestern und eines von morgen
|
||||
// sind beide "gerade jetzt relevant", eines von vor zwei Wochen oder in zwei
|
||||
// Monaten ist es nicht — unabhängig davon, ob es in der Vergangenheit oder
|
||||
// Zukunft liegt). wandAktuellInhalt() zeigt daraus die ersten WAND_FEED_MAX.
|
||||
// Jeder Eintrag trägt projekt/beschreibung getrennt (statt einem fertigen
|
||||
// Text) sowie pid/abschnitt für den Sprung ins
|
||||
// Projekt. Parameter: w. Rückgabe: Array aus {art, projekt (HTML, bereits
|
||||
// escaped), beschreibung (HTML, bereits escaped), zeit, pid, abschnitt}.
|
||||
function wandAktuelleEreignisse(w){
|
||||
// ===========================================================================
|
||||
// Normalansicht "Wandtafel in Glas" (Eriks Wahl aus dem Entwurf): gleicher
|
||||
// Hintergrund wie die App (Lichtflächen + Logo, styles.css, Block
|
||||
// "WANDTAFEL IN GLAS"), Schrift Outfit wie die Kalenderwoche der Login-Seite.
|
||||
// Jedes Element ist für sich antippbar und führt in dieselben Übersichten
|
||||
// bzw. Projekte wie zuvor.
|
||||
// ===========================================================================
|
||||
|
||||
// wtKopfHtml: Logo + "Dashboard", Sitzung, heute Abwesende (antippbar →
|
||||
// Übersicht "abwesend"), große Uhr mit Datum (#wtUhr, sekündlich von
|
||||
// wandCountdownTick() nachgeführt). Parameter: w. Rückgabe: HTML.
|
||||
function wtKopfHtml(w){
|
||||
const d = wandHeuteDaten(w);
|
||||
const jetzt = new Date();
|
||||
const abw = d.abwesend.length
|
||||
? '<div class="wt-abw" onclick="wandZeigeUebersicht(\'abwesend\')" title="Heute abwesend"><span>Heute abwesend</span>'
|
||||
+ d.abwesend.slice(0, 6).map(p => '<span class="wt-ava">' + esc(initialsFor(p.name)) + '</span>').join('')
|
||||
+ (d.abwesend.length > 6 ? '<span class="wt-ava">+' + (d.abwesend.length - 6) + '</span>' : '') + '</div>'
|
||||
: '<div class="wt-abw ist-leer">Heute alle da</div>';
|
||||
return '<header class="wt-feld wt-kopf">'
|
||||
+ '<div class="wt-marke"><img src="/logo.svg" alt=""><b>Dashboard</b></div>'
|
||||
+ '<span class="wt-sitzung">' + esc(w.label) + '</span>'
|
||||
+ abw
|
||||
+ '<div class="wt-uhr"><span id="wtUhr">' + esc(jetzt.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })) + '</span>'
|
||||
+ '<small>' + esc(jetzt.toLocaleDateString('de-DE', { weekday: 'long', day: 'numeric', month: 'long' })) + '</small></div>'
|
||||
+ '</header>';
|
||||
}
|
||||
|
||||
// wtAmpel: Farbe zur Auslastungsquote — dieselben Schwellen wie überall
|
||||
// (schwelleGelb()/schwelleRot(), js/load/load.js).
|
||||
function wtAmpel(quote){
|
||||
// Eine Null bekommt KEINE Ampelfarbe (dieselbe Regel wie im Auslastungs-
|
||||
// Tab): Grün hieße "gut ausgelastet", die Null heißt "nichts erfasst".
|
||||
if(quote === null || quote === undefined || quote <= 0) return 'var(--wt-tinte-3)';
|
||||
return quote >= schwelleRot() ? 'var(--wt-rot)' : quote >= schwelleGelb() ? 'var(--wt-gelb)' : 'var(--wt-gruen)';
|
||||
}
|
||||
|
||||
// wtKpiHtml: Auslastung gesamt und Besprochen mit Ring, offene Aufgaben
|
||||
// und Überfälliges (beide antippbar wie bisher). Parameter: w. Rückgabe: HTML.
|
||||
function wtKpiHtml(w){
|
||||
const g = wandGesamtKennzahlen(w);
|
||||
const heute = wandHeuteDaten(w);
|
||||
const bespr = wandBesprochenDaten(w);
|
||||
let ja = 0, n = 0;
|
||||
WAND_GEWERKE.forEach(x => { ja += bespr[x].besprochen; n += bespr[x].gesamt; });
|
||||
const ring = (pct, farbe) => '<span class="wt-ring" style="--p:' + Math.max(0, Math.min(100, pct)) + ';--c:' + farbe + '"></span>';
|
||||
const ausl = g.auslastung === null ? '—' : Math.round(g.auslastung * 100) + '<small>%</small>';
|
||||
const ueber = heute.ueberfaelligeListe.length;
|
||||
return '<section class="wt-kpis">'
|
||||
+ '<div class="wt-feld wt-kpi">' + ring(g.auslastung === null ? 0 : g.auslastung * 100, wtAmpel(g.auslastung))
|
||||
+ '<div><div class="wt-wert">' + ausl + '</div><div class="wt-lab"><b>Auslastung</b>alle Gewerke</div></div></div>'
|
||||
+ '<div class="wt-feld wt-kpi">' + ring(n ? ja / n * 100 : 0, 'var(--wt-gruen)')
|
||||
+ '<div><div class="wt-wert">' + ja + '<small>/' + n + '</small></div><div class="wt-lab"><b>Besprochen</b>diese Woche</div></div></div>'
|
||||
+ '<div class="wt-feld wt-kpi ist-klickbar" onclick="wandZeigeUebersicht(\'offen\')">'
|
||||
+ '<div><div class="wt-wert">' + heute.offen + '</div><div class="wt-lab"><b>Offene Aufgaben</b>in allen Projekten</div></div></div>'
|
||||
+ '<div class="wt-feld wt-kpi ist-klickbar' + (ueber ? ' ist-rot' : '') + '" onclick="wandZeigeUebersicht(\'ueberfaellig\')">'
|
||||
+ '<div><div class="wt-wert">' + ueber + '</div><div class="wt-lab"><b>Überfällig</b>' + (ueber ? 'Frist gerissen' : 'nichts gerissen') + '</div></div></div>'
|
||||
+ '</section>';
|
||||
}
|
||||
|
||||
// wtGewerkeHtml: je Gewerk eine Zeile mit Auslastung (Balken + Prozent,
|
||||
// antippbar → Übersicht "auslastung") und Besprochen-Stand (ein Kästchen je
|
||||
// Projekt, antippbar → Übersicht "besprochen"). Parameter: w. Rückgabe: HTML.
|
||||
function wtGewerkeHtml(w){
|
||||
const ausl = wandAuslastungJeGewerk(w);
|
||||
const bespr = wandBesprochenDaten(w);
|
||||
const personen = {};
|
||||
(Array.isArray(w.people) ? w.people : []).forEach(p => {
|
||||
const g = typeof personGewerk === 'function' ? personGewerk(p) : '';
|
||||
if(g) personen[g] = (personen[g] || 0) + 1;
|
||||
});
|
||||
const zeilen = WAND_GEWERKE.map(g => {
|
||||
const a = ausl[g], b = bespr[g];
|
||||
const quote = (a.bewertbar > 0 && a.kapazitaet > 0) ? a.stunden / a.kapazitaet : null;
|
||||
const farbe = wtAmpel(quote);
|
||||
const auslWert = quote === null ? '—' : Math.round(quote * 100) + ' %';
|
||||
const kaestchen = b.gesamt
|
||||
? Array.from({ length: b.gesamt }, (_, i) => '<i class="' + (i < b.besprochen ? 'ja' : '') + '"></i>').join('')
|
||||
: '<span class="wt-leer">keine Projekte</span>';
|
||||
return '<div class="wt-gw">'
|
||||
+ '<span class="wt-gew">' + esc(g) + '<small>' + (personen[g] || 0) + ' Personen</small></span>'
|
||||
+ '<div class="wt-gw-werte">'
|
||||
+ '<div class="wt-gw-zeile ist-klickbar" onclick="wandZeigeUebersicht(\'auslastung\',\'' + g + '\')">'
|
||||
+ '<span class="wt-gw-lab">Auslastung</span>'
|
||||
+ '<span class="wt-balken"><i style="width:' + (quote === null ? 0 : Math.min(100, quote * 100)) + '%;background:' + farbe + '"></i></span>'
|
||||
+ '<span class="wt-zahl" style="color:' + farbe + '">' + auslWert + '</span></div>'
|
||||
+ '<div class="wt-gw-zeile ist-klickbar" onclick="wandZeigeUebersicht(\'besprochen\',\'' + g + '\')">'
|
||||
+ '<span class="wt-gw-lab">Besprochen</span>'
|
||||
+ '<span class="wt-punkte">' + kaestchen + '</span>'
|
||||
+ '<span class="wt-zahl klein">' + (b.gesamt ? b.besprochen + '/' + b.gesamt : '—') + '</span></div>'
|
||||
+ '</div></div>';
|
||||
}).join('');
|
||||
return '<div class="wt-feld wt-panel wt-gewerke"><h2>Gewerke <span>Auslastung und Wochenbericht</span></h2>' + zeilen + '</div>';
|
||||
}
|
||||
|
||||
// WT_TAGE / WT_JE_TAG: wie viele Arbeitstage die Tagesansicht zeigt und wie
|
||||
// viele Einträge je Tag offen dastehen (der Rest als "+ n weitere").
|
||||
const WT_TAGE = 5;
|
||||
const WT_JE_TAG = 8;
|
||||
|
||||
// wtTageDaten: die nächsten WT_TAGE Arbeitstage ab heute (Wochenenden und
|
||||
// gepflegte Feiertage übersprungen, isWorkday()) mit den Meilensteinen aller
|
||||
// Projekte der Sitzung.
|
||||
// Parameter: w. Rückgabe: Array von { iso, datum: Date, heute, eintraege }.
|
||||
function wtTageDaten(w){
|
||||
const heute = heuteIso();
|
||||
const prevWeek = (typeof findPreviousWeek === 'function') ? findPreviousWeek(w) : null;
|
||||
const ereignisse = [];
|
||||
|
||||
const tage = [];
|
||||
let d = parseDateISO(heute);
|
||||
if(!isWorkday(d)) { while(!isWorkday(d)) d = addDays(d, 1); }
|
||||
while(tage.length < WT_TAGE){
|
||||
if(isWorkday(d)) tage.push({ iso: formatDateISO(d), datum: new Date(d), heute: formatDateISO(d) === heute, eintraege: [] });
|
||||
d = addDays(d, 1);
|
||||
}
|
||||
const jeTag = {}; tage.forEach(t => { jeTag[t.iso] = t; });
|
||||
const erster = tage[0];
|
||||
// Termine an Wochenenden und Feiertagen zwischen den Spalten gehören zum
|
||||
// Arbeitstag DAVOR (ein Termin am Samstag ist praktisch einer am Freitag).
|
||||
// Ohne diese Zuordnung fiel ein solcher Eintrag aus der Tafel heraus, weil
|
||||
// es für sein Datum keine Spalte gibt. Der Eintrag nennt dann das
|
||||
// eigentliche Datum.
|
||||
const spalteFuer = {};
|
||||
const WT = ['So','Mo','Di','Mi','Do','Fr','Sa'];
|
||||
for(let x = parseDateISO(erster.iso), ende = parseDateISO(tage[tage.length - 1].iso); x <= ende; x = addDays(x, 1)){
|
||||
const iso = formatDateISO(x);
|
||||
spalteFuer[iso] = jeTag[iso] || tage.filter(t => t.iso < iso).pop() || erster;
|
||||
}
|
||||
const hinweisFuer = iso => jeTag[iso] ? '' : 'am ' + WT[parseDateISO(iso).getDay()] + ' ' + parseDateISO(iso).getDate() + '.' + (parseDateISO(iso).getMonth() + 1) + '. · ';
|
||||
(Array.isArray(w.projects) ? w.projects : []).forEach(pr => {
|
||||
if(pr.group === 'In Anbahnung') return;
|
||||
const projekt = esc(pr.nr || '') + (pr.nr ? ' ' : '') + esc(pr.name || '');
|
||||
|
||||
if(typeof classifyProjectChange === 'function'){
|
||||
const klass = classifyProjectChange(pr, prevWeek);
|
||||
if(klass.bucket === 'status' || klass.bucket === 'new'){
|
||||
const vorschauRoh = pr.status ? statusPreviewText(pr.status) : '';
|
||||
const vorschau = vorschauRoh.slice(0, 70);
|
||||
ereignisse.push({
|
||||
art: 'neu',
|
||||
datum: pr.statusAt || heute,
|
||||
pid: pr.id, abschnitt: 'dashboard', projekt,
|
||||
beschreibung: (klass.bucket === 'new' ? 'Neues Projekt' : 'Neuer Kommentar')
|
||||
+ (vorschau ? ': „' + esc(vorschau) + (vorschauRoh.length > 70 ? '…' : '') + '"' : ''),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const todos = (pr.plan && Array.isArray(pr.plan.todos)) ? pr.plan.todos : [];
|
||||
todos.forEach(t => {
|
||||
if(t.done || !t.end || t.end >= heute) return;
|
||||
ereignisse.push({
|
||||
art: 'ueberfaellig',
|
||||
datum: t.end,
|
||||
pid: pr.id, abschnitt: 'aufgaben', projekt,
|
||||
beschreibung: '„' + esc(t.title || 'Aufgabe') + '" überfällig',
|
||||
});
|
||||
});
|
||||
|
||||
const meilensteine = (pr.plan && Array.isArray(pr.plan.milestones)) ? pr.plan.milestones : [];
|
||||
meilensteine.forEach(m => {
|
||||
if(!m.date || m.date < heute) return;
|
||||
ereignisse.push({
|
||||
art: 'termin',
|
||||
datum: m.date,
|
||||
pid: pr.id, abschnitt: 'terminplan', projekt,
|
||||
beschreibung: 'Meilenstein „' + esc(m.name || 'Meilenstein') + '"',
|
||||
});
|
||||
const projekt = (pr.nr ? pr.nr + ' ' : '') + (pr.name || '');
|
||||
const plan = pr.plan || {};
|
||||
// Eriks Wunsch: In den Tagesspalten stehen NUR Meilensteine. Aufgaben
|
||||
// (offen/überfällig) sind über die Kennzahlen oben erreichbar, die heute
|
||||
// Abwesenden über die Kopfleiste.
|
||||
(plan.milestones || []).forEach(m => {
|
||||
if(m.date && m.date >= heute && spalteFuer[m.date]) spalteFuer[m.date].eintraege.push({ art: 'ms', titel: m.name || 'Meilenstein', projekt: hinweisFuer(m.date) + projekt, pid: pr.id, abschnitt: 'terminplan', sort: m.date });
|
||||
});
|
||||
});
|
||||
|
||||
ereignisse.forEach(e => { e.zeit = relativerTag(e.datum); });
|
||||
ereignisse.sort((a, b) => Math.abs(tageBis(a.datum)) - Math.abs(tageBis(b.datum)));
|
||||
return ereignisse;
|
||||
tage.forEach(t => t.eintraege.sort((a, b) => String(a.sort).localeCompare(String(b.sort))));
|
||||
return tage;
|
||||
}
|
||||
|
||||
// wandAktuellInhalt: der Inhalt des Panels "Aktuell" (wandPanel() rahmt
|
||||
// ihn) — eine flache, chronologisch sortierte Liste (wandAktuelleEreignisse()
|
||||
// ist bereits nach zeitlicher Nähe zu heute sortiert), gedeckelt auf
|
||||
// WAND_FEED_MAX Einträge. Ein farbiger Punkt zeigt die Art
|
||||
// (Neu/Überfällig/Termin). Jeder Eintrag bleibt für sich antippbar und
|
||||
// springt direkt ins zugehörige Projekt. Parameter: w. Rückgabe:
|
||||
// HTML-String.
|
||||
function wandAktuellInhalt(w){
|
||||
const ereignisse = wandAktuelleEreignisse(w).slice(0, WAND_FEED_MAX);
|
||||
if(ereignisse.length === 0){
|
||||
return '<div class="wand-leer-klein">Nichts Neues diese Woche.</div>';
|
||||
}
|
||||
return ereignisse.map(e =>
|
||||
'<div class="dash-feed-eintrag" onclick="wandSpringeZuProjekt(\'' + esc(e.pid) + '\',\'' + esc(e.abschnitt) + '\')">'
|
||||
+ '<span class="dash-feed-punkt ' + e.art + '"></span>'
|
||||
+ '<div class="dash-feed-text">' + e.projekt + ' — ' + e.beschreibung
|
||||
+ '<span class="dash-feed-zeit">' + esc(e.zeit) + '</span></div>'
|
||||
+ '</div>'
|
||||
).join('');
|
||||
// wtTageHtml: die Spalten der nächsten Tage, nur mit Meilensteinen (Eriks
|
||||
// Wunsch). Ein Eintrag springt in den Terminplan seines Projekts.
|
||||
// Parameter: w. Rückgabe: HTML.
|
||||
function wtTageHtml(w){
|
||||
const tage = wtTageDaten(w);
|
||||
const WT = ['So','Mo','Di','Mi','Do','Fr','Sa'];
|
||||
const gruppe = {};
|
||||
const spalten = tage.map(t => {
|
||||
const sichtbar = t.eintraege.slice(0, WT_JE_TAG);
|
||||
const rest = t.eintraege.length - sichtbar.length;
|
||||
let letzteGruppe = null;
|
||||
const eintraege = sichtbar.map(e => {
|
||||
let kopf = '';
|
||||
if(t.heute && gruppe[e.art] && e.art !== letzteGruppe){ kopf = '<div class="wt-gruppe wt-' + e.art + '">' + gruppe[e.art] + '</div>'; letzteGruppe = e.art; }
|
||||
const klick = e.pid ? ' onclick="wandSpringeZuProjekt(\'' + esc(e.pid) + '\',\'' + e.abschnitt + '\')"' : '';
|
||||
return kopf + '<div class="wt-ein wt-' + e.art + (e.pid ? ' ist-klickbar' : '') + '"' + klick + '><i></i><div><b>' + esc(e.titel) + '</b><span>' + esc(e.projekt) + '</span></div></div>';
|
||||
}).join('');
|
||||
// "+ n weitere" steht fest am unteren Rand; wtSpaltenEinpassen() zählt
|
||||
// nach dem Zeichnen noch die Einträge hinzu, die nicht in die Höhe passen.
|
||||
const mehr = '<div class="wt-mehr" data-rest="' + rest + '"' + (t.heute ? ' onclick="wandZeigeUebersicht(\'ueberfaellig\')"' : '') + (rest > 0 ? '' : ' hidden') + '>+ ' + rest + ' weitere</div>';
|
||||
const titel = t.heute ? 'Heute' : WT[t.datum.getDay()];
|
||||
const datum = (t.heute ? WT[t.datum.getDay()] + ' ' : '') + t.datum.getDate() + '.' + (t.datum.getMonth() + 1) + '.';
|
||||
return '<div class="wt-tag' + (t.heute ? ' ist-heute' : '') + '"><div class="wt-tag-kopf"><b>' + titel + '</b><span>' + datum + '</span></div>'
|
||||
+ '<div class="wt-tag-liste">' + (eintraege || '<div class="wt-leer">Kein Meilenstein.</div>') + '</div>' + mehr + '</div>';
|
||||
}).join('');
|
||||
return '<div class="wt-feld wt-panel wt-tage"><h2>Die nächsten Tage <span>Meilensteine aller Projekte</span></h2>'
|
||||
+ '<div class="wt-tage-raster">' + spalten + '</div>'
|
||||
+ '</div>';
|
||||
}
|
||||
|
||||
// wtSpaltenEinpassen: blendet in jeder Tagesspalte die Einträge aus, die
|
||||
// nicht mehr in die Höhe passen (die Spaltenhöhe hängt vom Bildschirm ab,
|
||||
// zweizeilige Titel brauchen mehr Platz), und zählt sie zu "+ n weitere".
|
||||
// Läuft nach jedem Zeichnen der Normalansicht. Keine Parameter.
|
||||
function wtSpaltenEinpassen(){
|
||||
document.querySelectorAll('#wandContent .wt-tag').forEach(tag => {
|
||||
const liste = tag.querySelector('.wt-tag-liste');
|
||||
const mehr = tag.querySelector('.wt-mehr');
|
||||
if(!liste || !mehr) return;
|
||||
let versteckt = 0;
|
||||
const eintraege = [...liste.querySelectorAll('.wt-ein')];
|
||||
while(liste.scrollHeight > liste.clientHeight + 1 && eintraege.length){
|
||||
const e = eintraege.pop();
|
||||
e.hidden = true; versteckt++;
|
||||
// Eine Gruppenüberschrift ohne sichtbaren Eintrag danach mit ausblenden.
|
||||
const vor = e.previousElementSibling;
|
||||
if(vor && vor.classList.contains('wt-gruppe') && (!vor.nextElementSibling || vor.nextElementSibling.hidden)) vor.hidden = true;
|
||||
}
|
||||
const rest = (Number(mehr.dataset.rest) || 0) + versteckt;
|
||||
mehr.textContent = '+ ' + rest + ' weitere';
|
||||
mehr.hidden = rest === 0;
|
||||
});
|
||||
}
|
||||
|
||||
// wtFussHtml: Hinweis auf die Projektliste darunter (zum Wischen).
|
||||
function wtFussHtml(w){
|
||||
const n = (w.projects || []).length;
|
||||
return '<div class="wt-feld wt-fuss"><span class="wt-pfeil">↓</span><span>Wischen für alle ' + n + ' Projekte</span></div>';
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user