Plan Checker, Wochenbericht-Fokus-Modus und Wandtafel-Dashboard-Feinschliff

Plan Checker (neues Studenten-Werkzeug, Seitenleiste + Admin → Plan-Checker-
Merkmale): lädt einen Bauplan als PDF hoch und lässt ChatGPT oder Claude ihn
gegen eine gepflegte Merkmalsliste prüfen. PDF-Rendering (pdf.js +
@napi-rs/canvas) berücksichtigt jetzt Ebenen (Optional Content Groups) und
WASM-Decoder korrekt, sendet eine Übersicht plus mehrere hochaufgelöste
Kachel-Ausschnitte statt eines einzelnen, zu groben Bilds, und beide Anbieter
bekommen dasselbe Bildmaterial. Jedes Prüfergebnis kann auf eines der
gesendeten Bilder als Beleg verweisen (bildIndex) — als Screenshot im
Ergebnis anklickbar, öffnet vergrößert in einem Modal statt eines von Chrome
blockierten target=_blank-Links auf eine data:-URL. KI-Modellname ist jetzt
pro Konto überschreibbar (Einstellungen → Mein Konto) für Drittanbieter mit
abweichendem Modell-Katalog.

Wochenbericht: dichte Kartenansicht bekommt einen Fokus-Modus — nur der
Kommentar liegt offen, Leistungsphasen und Beteiligte stecken in
aufklappbaren Abschnitten statt dauerhaft nebeneinander zu stehen.

Projektbereich: Terminplan-Auslastung rechnet jetzt exakt wie der
Auslastungs-Tab/die Wandtafel (statt einer unabhängigen, tageweisen
Schätzung); die LPH-Fortschrittsliste im Dashboard zeigt nur noch die
beauftragten Leistungsphasen.

Wandtafel: Dashboard-Kopf mit Kennzahlen-Reihe und Panel-Reihe neu
strukturiert, in den Blautönen des Firmenlogos; dieselbe Palette wurde
anschließend auf den gesamten App-weiten Dark Mode übernommen.

Dateibereinigung: nicht mehr benötigte Test-Uploads/Backups entfernt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
erikandClaude Sonnet 5 committed 2026-10-05 14:01:41 +02:00
1 parent db63a29365
commit 6ba93b2a92
308 files changed
+64441 -743

No files matched your search

+163 -133
View File
@@ -9,19 +9,29 @@
// Touch-Display mit einem eigens dafür angelegten Kiosk-Konto — siehe
// CLAUDE.md "Wandtafel".
//
// Zweispaltig (Eriks Wahl "Variante C" aus dem Layout-Vergleich): links drei
// kompakte Kennzahl-Kacheln, rechts der Ereignis-Feed — seit einem zweiten,
// eigenen Vergleich (Feed-Vergleich) nach Art in drei Spalten gruppiert
// (Neu/Überfällig/Termine) statt chronologisch gemischt, siehe
// wandFeedHtml(). Die Kacheln selbst sind NICHT antippbar (kein Auf-/Zuklappen
// mehr, auf Eriks Wunsch) — stattdessen ist jedes einzelne darin dargestellte
// Element für sich antippbar (eine Gewerk-Zeile, eine Kennzahl, ein Kürzel,
// ein Feed-Eintrag) und führt in eine Vollbild-Übersicht bzw. direkt ins
// Aufbau: aus einem Vergleich mehrerer komplett unterschiedlicher
// Gesamtentwürfe wählte Erik die "Leitstand"-Struktur (Kopf → eine
// Kennzahlen-Reihe aus vier Kacheln → eine Reihe aus drei Panels), dann in
// einem zweiten Durchgang die konkrete Gestaltung ("Pastell Freundlich"),
// zuletzt umgefärbt in die zwei Farben des Firmenlogos (aus public/logo.ico
// ausgelesen) statt einer generischen Pastellmischung — siehe die
// --dash-*-Eigenschaften in styles.css. Kopf: Marke "Dashboard" + Sitzung/
// Uhrzeit + wer heute abwesend ist (renderWandDashKopfHtml()). Kennzahlen-
// Reihe: Auslastung gesamt, Besprochen gesamt, offene Aufgaben, überfällig
// (wandKpiReiheHtml()/wandGesamtKennzahlen()). Panel-Reihe: Auslastung je
// Gewerk, Besprochen je Gewerk, Aktuell — ein chronologisch sortierter
// Ereignis-Feed (wandAuslastungInhalt()/wandBesprochenInhalt()/
// wandAktuellInhalt(), gemeinsam gerahmt von wandPanel()). Die Kacheln/
// Panels selbst sind NICHT antippbar (kein Auf-/Zuklappen, auf Eriks
// Wunsch) — stattdessen ist jedes einzelne darin dargestellte Element für
// sich antippbar (eine Gewerk-Zeile, eine Kennzahl, ein Kürzel, ein
// Feed-Eintrag) und führt in eine Vollbild-Übersicht bzw. direkt ins
// 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()/
// kapazitaetsFaktor()/stundenFormat()/lpActiveList() aus js/load/load.js,
// kapazitaetsFaktor()/stundenFormat()/lpActiveList()/schwelleGelb()/
// schwelleRot() aus js/load/load.js,
// classifyProjectChange()/findPreviousWeek()/springZumWochenbericht() aus
// js/wochenbericht/wochenbericht.js bzw. js/plan/plan-info.js,
// statusPreviewText()/renderBoardRow()/renderBoardDrawer()/BOARD_OPTS aus
@@ -35,8 +45,8 @@
// Projektbereich, Seitenleiste und Kopfzeile kommen dabei zurück — wie bei
// jedem anderen Tab-Wechsel auch).
//
// Die Normalansicht ist höher als ein Bildschirm: unterhalb der zwei
// Kennzahl-Spalten folgt die vollständige Projektliste
// Die Normalansicht ist höher als ein Bildschirm: unterhalb von Kopf,
// Kennzahlen- und Panel-Reihe folgt die vollständige Projektliste
// (wandProjektlisteHtml()) — auf Eriks ausdrücklichen Wunsch optisch
// identisch mit der echten Projektliste (Tab "Projekte"), nicht eine eigene,
// vereinfachte Zeilenform: Jede Gruppe/Zeile entsteht über denselben
@@ -68,7 +78,7 @@
const WAND_GEWERKE = ['ELT', 'HLS', 'ARCH'];
const WAND_REFRESH_MS = 60000; // Hintergrund-Aktualisierung
const WAND_FEED_MAX = 5; // Höchstzahl Einträge JE SPALTE im Ereignis-Feed (Neu/Überfällig/Termine)
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
@@ -260,10 +270,10 @@ async function wandRefreshTick(){
load(true); // silent — ruft am Ende renderAll()+showTab(currentTab) auf
}
// renderWandboard: baut entweder die zweispaltige Normalansicht (mit der
// vollständigen Projektliste darunter) oder eine offene Vollbild-Übersicht
// auf. Wird von showTab('wand') sowie nach jedem load() (über
// showTab(currentTab)) aufgerufen.
// renderWandboard: baut entweder die Normalansicht (Kopf + Kennzahlen-Reihe +
// Panel-Reihe, mit der vollständigen Projektliste darunter) oder eine offene
// Vollbild-Übersicht auf. Wird von showTab('wand') sowie nach jedem load()
// (über showTab(currentTab)) aufgerufen.
//
// Die Normalansicht ist höher als ein Bildschirm — Erik hat einen echten
// Touch-Monitor und wollte "unterhalb der Wandtafel... weitere Informationen
@@ -295,18 +305,98 @@ function renderWandboard(opts){
}
container.classList.add('wand-scrollable');
container.innerHTML = '<div class="wand-ambient"><div class="wand-c-layout">'
+ '<div class="wand-c-links">' + wandLinkeSpalteHtml(w) + '</div>'
+ '<div class="wand-c-rechts">'
+ '<h3 class="wand-kachel-titel">Aktuell diese Woche</h3>'
+ '<div class="wand-feed-wrap">' + wandFeedHtml(w) + '</div>'
+ renderWandStand()
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>'
+ '</div>'
+ '</div></div>'
+ wandProjektlisteHtml(w);
container.scrollTop = vorherigerScroll;
}
// wandGesamtKennzahlen: zwei über alle Gewerke hinweg zusammengefasste
// Kennzahlen für die Kennzahlen-Reihe oben — dieselbe Rechnung wie die
// Gewerk-Aufschlüsselung darunter (wandAuslastungJeGewerk()/
// wandBesprochenDaten()), nur aufsummiert statt aufgeschlüsselt. Die
// bewusste Mehrfach-Gewerke-Dopplung (ein Projekt mit mehreren Gewerken
// zählt seinen vollen Beitrag bei jedem Gewerk) gilt hier automatisch mit,
// da beide Quellfunktionen sie bereits je Gewerk anwenden. Parameter: w.
// Rückgabe: {auslastung: Zahl|null, besprochen: Zahl|null} — null, wenn sich
// die Quote mangels Kapazität bzw. Projekten nicht bilden lässt.
function wandGesamtKennzahlen(w){
const ausl = wandAuslastungJeGewerk(w);
let stunden = 0, kapazitaet = 0;
WAND_GEWERKE.forEach(g => { stunden += ausl[g].stunden; kapazitaet += ausl[g].kapazitaet; });
const bespr = wandBesprochenDaten(w);
let besprochen = 0, gesamt = 0;
WAND_GEWERKE.forEach(g => { besprochen += bespr[g].besprochen; gesamt += bespr[g].gesamt; });
return {
auslastung: kapazitaet > 0 ? stunden / kapazitaet : null,
besprochen: gesamt > 0 ? besprochen / gesamt : null,
};
}
// 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,
@@ -350,26 +440,11 @@ function wandProjektlisteHtml(w){
}).join('');
return '<div class="wand-projektliste">'
+ '<h3 class="wand-kachel-titel">Alle Projekte</h3>'
+ '<h3 class="dash-panel-titel">Alle Projekte</h3>'
+ abschnitte
+ '</div>';
}
// wandLinkeSpalteHtml: die drei Kennzahl-Kacheln (nicht mehr antippbar —
// nur die Elemente darin sind es). Parameter: w. Rückgabe: HTML-String.
function wandLinkeSpalteHtml(w){
return wandTileAuslastung(w) + wandTileBesprochen(w) + wandTileHeute(w);
}
// wandKachel: die gemeinsame, nicht-interaktive Kachel-Umrahmung. Parameter:
// titel, inhalt (HTML). Rückgabe: HTML-String.
function wandKachel(titel, inhalt){
return '<section class="wand-kachel">'
+ '<h3 class="wand-kachel-titel">' + esc(titel) + '</h3>'
+ '<div class="wand-kachel-body">' + inhalt + '</div>'
+ '</section>';
}
// wandSpringeZuProjekt: verlässt die Wandtafel und öffnet den Projektbereich
// eines Projekts, optional direkt in einem bestimmten Abschnitt — derselbe
// Weg (Hash VOR dem Tab-Wechsel setzen, showTab('plan') übernimmt ihn über
@@ -496,14 +571,16 @@ function wandAuslastungJeGewerk(w){
return ergebnis;
}
// wandTileAuslastung: ein Meter je Gewerk, Ampelfarbe wie im Auslastungs-Tab.
// 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 wandTileAuslastung(w){
// 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);
const inhalt = WAND_GEWERKE.map(g => {
return WAND_GEWERKE.map(g => {
const d = daten[g];
const label = (DEPARTMENTS[g] && DEPARTMENTS[g].label) || g;
const klick = ' onclick="wandZeigeUebersicht(\'auslastung\',\'' + g + '\')"';
@@ -521,7 +598,6 @@ function wandTileAuslastung(w){
+ '<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('');
return wandKachel('Auslastung je Gewerk', inhalt);
}
// wandUebersichtAuslastung: alle Projekte, die zu einem Gewerk beitragen,
@@ -565,13 +641,16 @@ function wandBesprochenDaten(w){
return ergebnis;
}
// wandTileBesprochen: ein Meter je Gewerk ("X von Y"). Jede Zeile ist
// 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)). Parameter: w. Rückgabe:
// HTML-String.
function wandTileBesprochen(w){
// 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);
const inhalt = WAND_GEWERKE.map(g => {
return WAND_GEWERKE.map(g => {
const d = daten[g];
const label = (DEPARTMENTS[g] && DEPARTMENTS[g].label) || g;
if(d.gesamt === 0){
@@ -582,10 +661,9 @@ function wandTileBesprochen(w){
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(--accent)"></div></div>'
+ '<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('');
return wandKachel('Wochenbericht besprochen', inhalt);
}
// wandUebersichtBesprochen: die noch nicht besprochenen Projekte eines
@@ -653,27 +731,9 @@ function wandHeuteDaten(w){
return { offen: offeneListe.length, offeneListe, ueberfaelligeListe, abwesend };
}
// wandTileHeute: zwei Kennzahlen (offen/überfällig) plus Abwesenheits-Kürzel
// — jede einzeln antippbar, kein Auf-/Zuklappen der Kachel mehr. Parameter:
// w. Rückgabe: HTML-String.
function wandTileHeute(w){
const d = wandHeuteDaten(w);
const offenStat = '<div class="wand-stat ist-klickbar" onclick="wandZeigeUebersicht(\'offen\')" title="Alle offenen Aufgaben anzeigen">'
+ '<b>' + d.offen + '</b><span>offene Aufgaben</span></div>';
const ueberfaelligStat = '<div class="wand-stat' + (d.ueberfaelligeListe.length > 0 ? ' ist-rot' : '') + ' ist-klickbar"'
+ ' onclick="wandZeigeUebersicht(\'ueberfaellig\')" title="Alle überfälligen Aufgaben anzeigen">'
+ '<b>' + d.ueberfaelligeListe.length + '</b><span>überfällig</span></div>';
const namenHtml = d.abwesend.length
? '<div class="wand-namen">' + d.abwesend.map(p =>
'<span class="wand-avatar ist-klickbar" title="' + esc(p.name) + ' — Alle Abwesenheiten anzeigen" onclick="wandZeigeUebersicht(\'abwesend\')">'
+ esc(initialsFor(p.name)) + '</span>').join('') + '</div>'
: '<div class="wand-leer-klein">niemand abwesend</div>';
const inhalt = '<div class="wand-stat-reihe">' + offenStat + ueberfaelligStat + '</div>' + namenHtml;
return wandKachel('Heute', inhalt);
}
// wandTileHeute ist entfallen — "offene Aufgaben"/"überfällig" stehen jetzt
// in der Kennzahlen-Reihe (wandKpiReiheHtml()), das Abwesenheits-Kürzel im
// Kopf (wandDashKopfHtml()); beide lesen weiterhin aus wandHeuteDaten().
// wandUebersichtOffeneAufgaben: ALLE offenen Aufgaben (nicht nur
// überfällige), nächste Frist zuerst. Parameter: w. Rückgabe: HTML-String.
@@ -718,20 +778,18 @@ function wandUebersichtAbwesend(w){
return wandUebersichtRahmen('Abwesend heute (' + d.abwesend.length + ')', '<div class="wand-uebersicht-liste">' + zeilen + '</div>');
}
// ---------- Ereignis-Feed (rechte Spalte) ----------
// ---------- 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 Liste, nach
// zeitlicher Nähe zu heute sortiert (|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). wandFeedHtml() gruppiert diese EINE Liste anschließend nach `art`
// in drei Spalten (Variante C aus dem Feed-Vergleich) — die Sortierung
// bleibt deshalb unverändert, sie gilt nur jetzt INNERHALB jeder Spalte statt
// über alle Arten hinweg. Jeder Eintrag trägt projekt/beschreibung getrennt
// (statt einem fertigen Text) sowie pid/abschnitt für den Sprung ins
// ü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){
@@ -786,51 +844,23 @@ function wandAktuelleEreignisse(w){
return ereignisse;
}
// WAND_FEED_SPALTEN: Reihenfolge und Beschriftung der drei Feed-Spalten.
const WAND_FEED_SPALTEN = [
{ art: 'neu', label: 'Neu' },
{ art: 'ueberfaellig', label: 'Überfällig' },
{ art: 'termin', label: 'Termine' },
];
// wandFeedHtml: gruppiert wandAktuelleEreignisse() nach Art in drei Spalten
// (Eriks Wahl "Variante C" aus dem Feed-Vergleich) — zeigt auf einen Blick,
// wie viele Ereignisse welcher Art anstehen, statt sie chronologisch zu
// mischen. Die Spaltenüberschrift nennt die TATSÄCHLICHE Gesamtzahl dieser
// Art, auch wenn nur WAND_FEED_MAX Einträge angezeigt werden. Jeder Eintrag
// bleibt für sich antippbar und springt direkt ins zugehörige Projekt.
// Parameter: w. Rückgabe: HTML-String.
function wandFeedHtml(w){
const ereignisse = wandAktuelleEreignisse(w);
const spalten = WAND_FEED_SPALTEN.map(s => {
const alle = ereignisse.filter(e => e.art === s.art);
const gezeigt = alle.slice(0, WAND_FEED_MAX);
const eintraegeHtml = gezeigt.length
? gezeigt.map(e =>
'<div class="wand-feed-eintrag" onclick="wandSpringeZuProjekt(\'' + esc(e.pid) + '\',\'' + esc(e.abschnitt) + '\')">'
+ '<b>' + e.projekt + '</b><span>' + e.beschreibung + '</span>'
+ '<span class="wand-feed-zeit">' + esc(e.zeit) + '</span>'
+ '</div>'
).join('')
: '<div class="wand-leer-klein">keine</div>';
return '<div class="wand-feed-gruppe">'
+ '<div class="wand-feed-gruppe-kopf">'
+ '<span class="wand-feed-gruppe-punkt ' + s.art + '"></span>'
+ '<span class="wand-feed-gruppe-titel">' + esc(s.label) + '</span>'
+ '<span class="wand-feed-gruppe-anzahl">' + alle.length + '</span>'
+ '</div>'
+ eintraegeHtml
+ '</div>';
}).join('');
return '<div class="wand-feed-gruppen">' + spalten + '</div>';
}
// ---------- Stand ----------
// renderWandStand: reiner Zeitstempel der letzten erfolgreichen
// Aktualisierung — das einzige Lebenszeichen, dass das unbeaufsichtigte
// Display nicht eingefroren ist. Keine Parameter. Rückgabe: HTML-String.
function renderWandStand(){
const jetzt = new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
return '<div class="wand-stand">Stand: ' + jetzt + ' Uhr</div>';
// 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('');
}