// -----------------------------
// Projektübersicht — Dashboard ("Meine Aufgaben"-Tab)
// -----------------------------
// Modul für den ersten Tab der Hauptseite (App.html, Ladereihenfolge:
// ... → load → dashboard → gantt → ...). Das Dashboard ist die Startübersicht
// nach dem Login; alles darin stammt aus einem einzigen GET /my-dashboard.
//
// Der Aufbau folgt der Frage, die beim Blick darauf zuerst ansteht: "Was muss
// ich heute anpacken?"
//
// 1. KOPF Begrüßung und Datum links, Sitzung und Uhr klein rechts
// (renderDashboardKopf(), gefüllt von startDashboardClock()).
// 2. BAND Der Befund des Tages als SATZ, daneben vier Kennzahlen
// (renderDashboardBand()). Ist nichts überfällig und nichts
// heute fällig, bleibt das Band farblos.
// 3. LINKS "Meine Projekte": je Projekt, dem man zugeordnet ist, ein
// Block mit der eigenen Rolle, der laufenden Leistungsphase und
// den offenen Terminen des Projekts — eigene Aufgaben
// hervorgehoben (renderDashProjekte()). Projekte, in denen man
// nur eine einzelne Aufgabe hat, stehen ebenfalls dort.
// 4. RECHTS Ein Monatskalender mit allen Terminen der eigenen Projekte,
// den eigenen Aufgaben, Feiertagen und den Abwesenheiten der
// Projektbeteiligten; ein Klick auf einen Tag listet ihn
// (renderDashKalender()). Darunter ausstehende Freigaben und
// Abwesenheiten.
//
// Projekte und Kalender rechnen mit der bereits geladenen Sitzung
// (currentWeek(), /load-all) statt mit GET /my-dashboard: Der Kalender soll
// beliebig weit blättern können, der Server liefert Projekttermine aber nur
// 21 Tage voraus. Kopf und Band bleiben bei den Zahlen des Servers.
// Entwurf "B — Monatsblatt" aus Eriks Vergleich dreier Varianten.
//
// Das war zuvor anders und schlechter: Die Uhrzeit war die größte Zahl auf
// dem Bildschirm, vier gleich große Kennzahlkacheln zeigten meist drei Nullen
// und gaben der einen wichtigen Zahl kein Gewicht, eine einzelne
// Aufgabenkarte belegte eine halbe Bildschirmbreite für vier Zeilen Inhalt,
// und alles lag untereinander — der Urlaub eines Kollegen stand gleichrangig
// unter den eigenen offenen Aufgaben.
//
// Nutzt baseUrl() aus js/server.js, currentUser aus js/auth.js, PLAN_SLUG aus
// js/globals.js, esc()/formatWeekDate()/heuteIso()/isoPlusTage() aus
// js/utils.js.
// ABSENCE_TYPE_CLASS: ordnet den Abwesenheitstypen (siehe absenceEditType in
// Projektbereich: Urlaub/Krank/Fortbildung/Sonstiges) eine Farbklasse für
// das Badge in der Kollegen-Abwesenheitsliste zu; "Sonstiges" fällt auf die
// neutrale .absence-chip-Standardfarbe zurück (kein Eintrag nötig).
const ABSENCE_TYPE_CLASS = {
'Urlaub': 'absence-type-urlaub',
'Krank': 'absence-type-krank',
'Fortbildung': 'absence-type-fortbildung',
};
// dashboardClockTimer: Intervall-Handle der Begrüßungs-Uhr. Wird bei jedem
// renderDashboard()-Aufruf neu gestartet (vorheriges Intervall zuerst
// gestoppt), damit beim wiederholten Tab-Wechsel nicht mehrere Intervalle
// parallel laufen.
let dashboardClockTimer = null;
// renderDashboard: lädt GET /my-dashboard und rendert Begrüßung/Uhr,
// Kennzahlen sowie die drei Bereiche in #dashboardContent. Wird nur beim
// Wechsel auf den Tab aufgerufen (showTab()), nicht beim initialen
// Seitenaufbau. Keine Parameter, keine Rückgabe.
async function renderDashboard(){
const container = document.getElementById('dashboardContent');
if(!container) return;
container.innerHTML = '
Lade Dashboard…
';
let data;
try {
const res = await fetch(baseUrl() + '/my-dashboard');
data = await res.json();
if(!res.ok || !data.ok) throw new Error((data && data.error) || ('HTTP ' + res.status));
} catch(err){
container.innerHTML = '
Dashboard konnte nicht geladen werden: ' + esc(err.message || String(err)) + '
';
return;
}
const weekLabel = data.weekLabel || null;
const tasks = Array.isArray(data.tasks) ? data.tasks : [];
const projectDates = Array.isArray(data.projectDates) ? data.projectDates : [];
const pending = Array.isArray(data.pendingReviews) ? data.pendingReviews : [];
const absences = Array.isArray(data.colleagueAbsences) ? data.colleagueAbsences : [];
const noPersonId = !!(currentUser && !currentUser.personId);
// Personen zählen, nicht Einträge: Wer heute "Urlaub" UND "krank" erfasst
// hat, ist eine abwesende Person, nicht zwei.
const absencesToday = new Set(absences.filter(a => a.current).map(a => a.personId || a.userId || a.name)).size;
// ---------- Aufbau ----------
//
// Die Anordnung folgt der Frage, die beim Blick aufs Dashboard zuerst
// ansteht: "Was muss ich heute anpacken?" Deshalb steht oben ein Befund im
// Klartext statt vier gleich großer Kacheln, darunter links der
// Arbeitsstrom (was ich tue) und rechts der Rand (was ich wissen muss).
//
// Zuvor war die Uhrzeit die größte Zahl auf dem Bildschirm, drei von vier
// Kennzahlen zeigten eine Null, eine einzelne Aufgabenkarte belegte eine
// halbe Bildschirmbreite für vier Zeilen Inhalt, und der Urlaub eines
// Kollegen stand gleichrangig unter den eigenen offenen Aufgaben.
const heute = heuteIso();
const eigeneUeberfaellig = tasks.filter(t => t.end && t.end < heute).length;
const ueberfaellig = eigeneUeberfaellig + projectDates.filter(d => d.ueberfaellig).length;
const heuteFaellig = tasks.filter(t => t.end === heute).length
+ projectDates.filter(d => d.heute).length;
let html = '';
html += renderDashboardKopf(weekLabel);
html += renderDashboardBand({
ueberfaellig, heuteFaellig,
aufgaben: tasks.length,
termine: projectDates.length,
freigaben: pending.length,
abwesend: absencesToday,
});
if(noPersonId){
html += '
Ihrem Benutzerkonto ist keine Person zugeordnet — es können daher keine Aufgaben angezeigt werden. Ein Administrator kann die Zuordnung in der Benutzerverwaltung ergänzen.
';
}
// Neu zugewiesene Aufgaben (js/benachrichtigungen.js) — vor allem anderen,
// weil es das ist, was sich seit dem letzten Blick geändert hat.
html += '';
html += '
';
// Linke Zone: die eigenen Projekte mit ihren Terminen
const daten = dashDaten();
html += '
';
}
// dashKalWaehle / dashKalBlaettern: Tag wählen bzw. Monat wechseln. Ein Tag
// aus dem Vor- oder Folgemonat blättert dorthin.
function dashKalWaehle(iso){
dashKalTag = iso;
const d = parseDateISO(iso);
dashKalMonat = { jahr: d.getFullYear(), monat: d.getMonth() };
renderDashKalender();
}
function dashKalBlaettern(n){
let { jahr, monat } = dashKalMonat;
monat += n;
if(monat < 0){ monat = 11; jahr--; }
if(monat > 11){ monat = 0; jahr++; }
dashKalMonat = { jahr, monat };
renderDashKalender();
}
// renderDashboardKopf: Begrüßung und Datum links, Sitzung und Uhr klein
// rechts. Die Uhrzeit war zuvor die größte Zahl auf dem Bildschirm — sie ist
// die Angabe, nach der man am wenigsten handelt, und steht deshalb am Rand.
// Die Inhalte füllt startDashboardClock() live nach.
// Parameter: weekLabel. Rückgabe: HTML-String.
function renderDashboardKopf(weekLabel){
return '
';
}
// renderDashboardBand: der Befund des Tages als Satz, daneben die Kennzahlen.
//
// Zuvor standen hier vier gleich große Kacheln, von denen meist drei eine
// Null zeigten — die eine Zahl, auf die es ankommt, bekam dadurch kein
// Gewicht. Der Satz benennt den Befund, die Zahlen bleiben daneben stehen.
// Ist nichts überfällig und nichts heute fällig, wird das Band farblos:
// Eine Null in Alarmfarbe behauptet ein Problem, das es nicht gibt.
// Parameter: z (Zählungen). Rückgabe: HTML-String.
function renderDashboardBand(z){
let satz;
if(z.ueberfaellig > 0){
satz = '' + z.ueberfaellig + ' '
+ (z.ueberfaellig === 1 ? 'Termin ist überfällig' : 'Termine sind überfällig')
+ (z.heuteFaellig ? ', ' + z.heuteFaellig + ' ' + (z.heuteFaellig === 1 ? 'wird' : 'werden') + ' heute fällig' : '')
+ '.';
} else if(z.heuteFaellig > 0){
satz = '' + z.heuteFaellig + ' ' + (z.heuteFaellig === 1 ? 'Termin wird' : 'Termine werden')
+ ' heute fällig, nichts ist überfällig.';
} else if(z.aufgaben > 0){
satz = 'Nichts überfällig. ' + z.aufgaben + ' offene '
+ (z.aufgaben === 1 ? 'Aufgabe' : 'Aufgaben') + ' in Arbeit.';
} else {
satz = 'Nichts offen, nichts überfällig.';
}
const zahl = (wert, label) =>
'
';
}
// dashAbschnitt / dashLeer: die immer gleiche Umrahmung eines Blocks —
// Überschrift, Anzahl, ein erklärender Zusatz am rechten Rand.
// Parameter: titel, zahl (oder null), zusatz (oder null), inhalt.
// kopfHtml (optional): fertiges HTML am rechten Rand der Überschrift, etwa
// ein Bedienknopf.
function dashAbschnitt(titel, zahl, zusatz, inhalt, kopfHtml){
return ''
+ '
';
}
// renderDashboardAbsences: die Abwesenheiten der Kollegen (aktuelle sowie in
// den nächsten 14 Tagen beginnende, siehe GET /my-dashboard). Dieselbe
// Zeilenform wie die Freigaben daneben — beides ist Randinformation, die man
// zur Kenntnis nimmt, ohne selbst tätig zu werden.
// Parameter: absences (Array). Rückgabe: HTML-String.
function renderDashboardAbsences(absences){
if(absences.length === 0){
return dashLeer('Keine Abwesenheiten in den nächsten 14 Tagen.');
}
const zeilen = absences.map(a => {
const zeitraum = a.start === a.end
? formatWeekDate(a.start)
: formatWeekDate(a.start) + ' – ' + formatWeekDate(a.end);
const typKlasse = ABSENCE_TYPE_CLASS[a.type] || '';
// Wer heute fehlt, ist die eigentliche Nachricht; die Art der Abwesenheit
// steht dann in der Zeile darunter.
const chip = a.current
? 'heute'
: '' + esc(a.type || 'Abwesend') + '';
return '
';
}
// initialsFor:// initialsFor: liefert bis zu zwei Buchstaben Initialen für einen Namen, als
// Avatar-Kürzel in der Kollegen-Abwesenheitsliste. Parameter: name (String).
// Rückgabe: String.
function initialsFor(name){
const parts = String(name || '').trim().split(/\s+/).filter(Boolean);
if(parts.length === 0) return '?';
if(parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
}