// -----------------------------
// 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.
// 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…
';
// Notizen je Projekt (Anzahl) parallel — fällt sie aus, fehlt nur die Angabe.
const notizenAnfrage = fetch(baseUrl() + '/projekt-notizen/anzahl')
.then(r => r.ok ? r.json() : null).catch(() => null);
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 nz = await notizenAnfrage;
dashNotizen = (nz && nz.ok && nz.anzahl) || {};
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;
const daten = dashDaten();
const sonntag = formatDateISO(new Date(dashWochenMontag().getTime() + 6 * 86400000));
const dieseWoche = daten.eintraege.filter(e => e.datum >= heute && e.datum <= sonntag).length;
let html = '';
html += renderDashboardKopf(weekLabel);
html += renderDashboardBand({
ueberfaellig, heuteFaellig, dieseWoche,
aufgaben: tasks.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 += renderDashWoche(daten);
html += '
';
// Linke Zone: die eigenen Projekte mit ihren Terminen
html += '
';
}
// renderDashWoche: Band „Diese Woche“ (Entwurf „Bauplan“): Mo bis Fr als
// Spalten mit den Terminen der eigenen Projekte an diesem Tag — eigene
// Aufgaben, Aufgaben im Projekt, Meilensteine, Überfälliges. Je Tag höchstens
// DASH_WOCHE_JE_TAG, der Rest als „+ N weitere“. Parameter: daten (dashDaten()).
const DASH_WOCHE_JE_TAG = 4;
function renderDashWoche(daten){
if(!daten.geladen || !daten.projekte.length) return '';
const heute = heuteIso();
const mo = dashWochenMontag();
const TAGE = ['Mo', 'Di', 'Mi', 'Do', 'Fr'];
let spalten = '';
for(let i = 0; i < 5; i++){
const d = new Date(mo); d.setDate(mo.getDate() + i);
const iso = formatDateISO(d);
const e = daten.eintraege.filter(x => x.datum === iso)
.sort((a, b) => (b.meins ? 1 : 0) - (a.meins ? 1 : 0));
const sicht = e.slice(0, DASH_WOCHE_JE_TAG);
const rest = e.length - sicht.length;
spalten += '
';
}
// 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 ''
+ '
' + inhalt + '';
}
function dashLeer(text){
return '
' + esc(text) + '
';
}
// relativerTag: "heute", "morgen", "in 5 Tagen", "seit 3 Tagen" zu einem
// ISO-Datum. Die Angabe steht neben dem Datum, weil "12.08." allein nicht
// sagt, ob das morgen oder in drei Wochen ist.
// Parameter: iso. Rückgabe: String.
function relativerTag(iso){
const d = tageBis(iso);
if(d === 0) return 'heute';
if(d === 1) return 'morgen';
if(d === -1) return 'seit 1 Tag';
if(d < 0) return 'seit ' + Math.abs(d) + ' Tagen';
return 'in ' + d + ' Tagen';
}
// tageBis: Kalendertage von heute bis zu einem ISO-Datum (negativ = liegt
// zurück). Parameter: iso. Rückgabe: ganze Zahl.
function tageBis(iso){
if(!iso) return 0;
const heute = new Date(); heute.setHours(0, 0, 0, 0);
const ziel = new Date(iso); ziel.setHours(0, 0, 0, 0);
return Math.round((ziel.getTime() - heute.getTime()) / 86400000);
}
// renderDashboardFreigabe: eine ausstehende Datei-Freigabe als schmale
// Zeile für die rechte Spalte. Parameter: pr. Rückgabe: HTML-String.
function renderDashboardFreigabe(pr){
const projekt = (pr.projectNr ? pr.projectNr + ' ' : '') + (pr.projectName || '');
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();
}