// -----------------------------
// 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 += '
');
html += dashAbschnitt('Abwesenheiten', absences.length, null,
renderDashboardAbsences(absences));
html += '
';
html += '
';
container.innerHTML = html;
if(typeof renderDashNeu === 'function'){ renderDashNeu(); ladeBenachrichtigungen(true); }
dashKalDaten = daten;
renderDashKalender();
}
// ---------- Meine Projekte + Kalender (Entwurf B "Monatsblatt") ----------
// dashKalDaten / dashKalMonat / dashKalTag: Zustand des Kalenders. Der Monat
// und der gewählte Tag überleben ein Neuladen des Dashboards nicht absichtlich
// nicht — wer wiederkommt, will "heute" sehen.
let dashKalDaten = null;
let dashKalMonat = null; // { jahr, monat } (monat 0–11)
let dashKalTag = null; // ISO-Datum
// dashNotizen: { project_uid: {anzahl, zuletzt} } aus GET /projekt-notizen/anzahl.
let dashNotizen = {};
// dashDaten: sammelt aus der aktuellen Sitzung (currentWeek()) die Projekte
// des angemeldeten Kontos und alle Kalendereinträge. Ein Projekt gehört dazu,
// wenn die Person ihm zugeordnet ist (assign, Haupt 1 / Unterstützung 2) oder
// dort eine eigene offene Aufgabe hat.
// Rückgabe: { projekte:[{p, rollen:[{rolle,state}], termine:[…]}], eintraege:[…] }.
function dashDaten(){
// geladen: false, solange /load-all noch nicht da ist — das Dashboard wird
// beim Anmelden oft schon vorher aufgebaut; load() zeichnet es danach neu.
const geladen = Array.isArray(db.weeks) && db.weeks.length > 0;
const leer = { projekte: [], eintraege: [], abwesenheiten: [], geladen };
const w = (geladen && typeof currentWeek === 'function') ? currentWeek() : null;
const ich = currentUser && currentUser.personId;
if(!w || !ich) return leer;
const heute = heuteIso();
const namen = {};
(w.people || []).forEach(x => { namen[x.id] = x.fullName || x.name || x.id; });
const projekte = [];
(w.projects || []).forEach(p => {
const rollen = [];
Object.keys(p.assign || {}).forEach(key => {
const [pid, rolle] = key.split('|');
const state = Number(p.assign[key]) || 0;
if(pid === ich && state > 0) rollen.push({ rolle: rolle || '', state });
});
const plan = p.plan || {};
const termine = [];
(plan.todos || []).forEach(t => {
if(t.done) return;
const meins = Array.isArray(t.assignees) && t.assignees.includes(ich);
// Fremde Aufgaben ohne Datum gehören in keinen Kalender und keine Terminliste.
if(!t.end && !meins) return;
termine.push({
art: 'aufgabe', titel: t.title || '(ohne Titel)', datum: t.end || '',
meins, progress: Number(t.progress) || 0,
wer: (t.assignees || []).map(id => namen[id] || id),
});
});
(plan.milestones || []).forEach(m => {
if(!m.date) return;
termine.push({ art: 'meilenstein', titel: m.name || 'Meilenstein', datum: m.date, meins: false, wer: [], erreicht: !!m.erreicht });
});
const hatEigene = termine.some(t => t.meins);
if(!rollen.length && !hatEigene) return;
// Erreichte Meilensteine fallen weg (im Kalender bleiben sie stehen);
// nicht erreichte vor heute gelten wie offene Aufgaben als überfällig —
// dieselbe Regel wie die Zahl im Band (GET /my-dashboard).
const relevant = termine.filter(t => !(t.art === 'meilenstein' && t.erreicht));
relevant.sort((a, b) => (a.datum || '9999').localeCompare(b.datum || '9999'));
projekte.push({ p, rollen, termine: relevant, alleTermine: termine });
});
// Reihenfolge wie in der Projektliste (Gruppe, dann Position darin) —
// oder die eigene, auf dem Dashboard gezogene (dashSortieren()).
dashSortieren(projekte);
// Kalendereinträge: alle datierten Termine dieser Projekte (auch vergangene
// Meilensteine — beim Zurückblättern sollen sie da sein).
const eintraege = [];
projekte.forEach(x => x.alleTermine.forEach(t => {
if(t.datum) eintraege.push(Object.assign({ projekt: x.p }, t));
}));
// Abwesenheiten: das eigene Konto und alle, die einem der eigenen Projekte
// zugeordnet sind — wer sonst fehlt, betrifft die eigene Arbeit nicht.
const beteiligt = new Set([ich]);
projekte.forEach(x => Object.keys(x.p.assign || {}).forEach(key => {
if(Number(x.p.assign[key]) > 0) beteiligt.add(key.split('|')[0]);
}));
const abwesenheiten = [];
(Array.isArray(db.globalPeople) ? db.globalPeople : []).forEach(person => {
if(!beteiligt.has(person.id)) return;
(person.absences || []).forEach(a => {
if(a.start && a.end) abwesenheiten.push({ name: person.id === ich ? 'Sie' : (person.fullName || person.name), start: a.start, end: a.end, type: a.type || 'Abwesend' });
});
});
return { projekte, eintraege, abwesenheiten, geladen };
}
// dashProjektUrl: Adresse des Projektbereichs (gleiches Schema wie
// openProjectPlan() in js/gantt.js). Parameter: p, abschnitt. Rückgabe: String.
function dashProjektUrl(p, abschnitt){
const w = currentWeek();
const url = new URL('/' + PLAN_SLUG + '/' + encodeURIComponent(p.name || ''), window.location.origin);
url.searchParams.set('weekLabel', w ? w.label : '');
url.searchParams.set('projNr', p.nr || '');
url.hash = abschnitt || 'aufgaben';
return url.toString();
}
// dashImTabOeffnen: öffnet den Projektbereich im SELBEN Tab (Eriks Wunsch),
// ohne die Seite neu zu laden. Strg/Cmd/Umschalt-Klick und Mittelklick
// behalten das Browser-Verhalten (neuer Tab), dafür bleibt das href stehen.
// Der Abschnitt (#aufgaben …) wird wie in springZuEinstellungen() VOR
// showTab('plan') gesetzt, dessen updateRouteUrl() ihn übernimmt.
function dashImTabOeffnen(ev, a){
if(ev.button !== 0 || ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey) return true;
ev.preventDefault();
const url = new URL(a.href);
const name = decodeURIComponent(url.pathname.split('/').pop() || '');
setPlanTarget(url.searchParams.get('weekLabel') || '', url.searchParams.get('projNr') || '', name);
location.hash = url.hash.slice(1) || 'dashboard';
showTab('plan');
window.scrollTo(0, 0);
return false;
}
// DASH_TERMINE_JE_PROJEKT: so viele Termine stehen je Projekt offen da,
// Überfälliges zuerst. Der Rest steckt hinter "weitere Termine" — sonst
// verdrängt ein Projekt mit 25 gerissenen Fristen alle anderen aus dem Blick
// (so im Bestand gesehen). Die Zahl der überfälligen steht ohnehin im Kopf.
const DASH_TERMINE_JE_PROJEKT = 6;
// Eingeklappte Projektblöcke (Eriks Wunsch): je Projekt merkt sich der
// Browser den Zustand über die dauerhafte projectUid — die Projekt-id ändert
// sich mit jeder Sitzung. Reiner Darstellungszustand, deshalb localStorage
// (Schlüssel DASH_ZU_KEY); ohne Speicher (privates Fenster) ist alles offen.
const DASH_ZU_KEY = 'projuebersicht-dash-zu';
function dashZuLesen(){
try { return new Set(JSON.parse(localStorage.getItem(DASH_ZU_KEY) || '[]')); } catch(e){ return new Set(); }
}
function dashZuSchreiben(set){
try { localStorage.setItem(DASH_ZU_KEY, JSON.stringify([...set])); } catch(e){ /* nur Komfort */ }
}
function dashProjKey(p){ return p.projectUid || p.nr || p.id; }
// ---------- Reihenfolge der Projekte ----------
// Grundlage ist die Reihenfolge der Projektliste (GROUP_ORDER, dann p.order).
// Jedes Konto kann sie auf dem Dashboard durch Ziehen am Griff ändern; die
// eigene Reihenfolge liegt am Server (users.dashboard_reihenfolge, über
// currentUser.dashboardReihenfolge), gilt also auf jedem Gerät. Projekte,
// die darin noch fehlen (neu zugeordnet), stehen nach ihrer Stelle in der
// Projektliste dahinter.
function dashListenRang(p){
const g = GROUP_ORDER.indexOf(p.group);
return (g === -1 ? 999 : g) * 1e6 + (Number(p.order) || 0);
}
function dashEigeneReihenfolge(){
return (currentUser && Array.isArray(currentUser.dashboardReihenfolge)) ? currentUser.dashboardReihenfolge : [];
}
function dashSortieren(projekte){
const eigen = dashEigeneReihenfolge();
const pos = new Map(eigen.map((k, i) => [k, i]));
projekte.sort((a, b) => {
const ia = pos.has(dashProjKey(a.p)) ? pos.get(dashProjKey(a.p)) : Infinity;
const ib = pos.has(dashProjKey(b.p)) ? pos.get(dashProjKey(b.p)) : Infinity;
if(ia !== ib) return ia - ib;
return dashListenRang(a.p) - dashListenRang(b.p);
});
}
function dashReihenfolgeSpeichern(liste){
if(currentUser) currentUser.dashboardReihenfolge = liste;
fetch(baseUrl() + '/dashboard-reihenfolge', {
method: 'POST',
headers: { 'Content-Type': 'application/json; charset=utf-8' },
body: JSON.stringify({ reihenfolge: liste })
}).then(r => { if(!r.ok) throw new Error(r.status); })
.catch(() => alert('Die Reihenfolge konnte nicht gespeichert werden.'));
}
// dashReihenfolgeZuruecksetzen: wieder wie in der Projektliste.
function dashReihenfolgeZuruecksetzen(){
dashReihenfolgeSpeichern([]);
renderDashboard();
}
// Ziehen: Der Block wird nur ziehbar, solange der Griff gedrückt ist —
// sonst ließe sich Text in den Terminen nicht mehr markieren.
let dashGezogen = null;
function dashGriffDruck(griff){
const block = griff.closest('.dash-proj');
if(block) block.draggable = true;
}
document.addEventListener('mouseup', () => {
if(dashGezogen) return;
document.querySelectorAll('.dash-proj[draggable="true"]').forEach(b => { b.draggable = false; });
});
document.addEventListener('dragstart', e => {
const block = e.target && e.target.closest && e.target.closest('.dash-proj[draggable="true"]');
if(!block) return;
dashGezogen = block;
block.classList.add('ist-gezogen');
e.dataTransfer.effectAllowed = 'move';
try { e.dataTransfer.setData('text/plain', block.dataset.key || ''); } catch(err){ /* egal */ }
});
document.addEventListener('dragover', e => {
if(!dashGezogen) return;
const liste = dashGezogen.parentElement;
if(!liste || !liste.contains(e.target)) return;
e.preventDefault();
const andere = [...liste.querySelectorAll('.dash-proj:not(.ist-gezogen)')];
const danach = andere.find(b => { const r = b.getBoundingClientRect(); return e.clientY < r.top + r.height / 2; });
if(danach) liste.insertBefore(dashGezogen, danach); else liste.appendChild(dashGezogen);
});
document.addEventListener('drop', e => { if(dashGezogen) e.preventDefault(); });
document.addEventListener('dragend', () => {
if(!dashGezogen) return;
const block = dashGezogen;
dashGezogen = null;
block.classList.remove('ist-gezogen');
block.draggable = false;
const neu = [...block.parentElement.querySelectorAll('.dash-proj')].map(b => b.dataset.key);
const alt = dashEigeneReihenfolge();
// Schon bekannte, gerade nicht angezeigte Projekte (z. B. vorübergehend
// ohne Zuordnung) behalten ihren Platz am Ende.
const liste = neu.concat(alt.filter(k => !neu.includes(k)));
if(JSON.stringify(liste) !== JSON.stringify(alt)) dashReihenfolgeSpeichern(liste);
const reset = document.getElementById('dashReihenfolgeReset');
if(reset) reset.hidden = false;
});
// dashVerschieben: Tastatur-Alternative zum Ziehen (Griff fokussiert,
// Pfeil hoch/runter).
function dashGriffTaste(e, griff){
if(e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return;
e.preventDefault();
const block = griff.closest('.dash-proj');
const liste = block && block.parentElement;
if(!liste) return;
if(e.key === 'ArrowUp' && block.previousElementSibling) liste.insertBefore(block, block.previousElementSibling);
else if(e.key === 'ArrowDown' && block.nextElementSibling) liste.insertBefore(block.nextElementSibling, block);
else return;
griff.focus();
const neu = [...liste.querySelectorAll('.dash-proj')].map(b => b.dataset.key);
const alt = dashEigeneReihenfolge();
dashReihenfolgeSpeichern(neu.concat(alt.filter(k => !neu.includes(k))));
const reset = document.getElementById('dashReihenfolgeReset');
if(reset) reset.hidden = false;
}
// dashProjUmschalten: klappt einen Projektblock auf oder zu — direkt am DOM,
// ohne das Dashboard neu zu laden. Parameter: knopf (der Kopf-Button).
function dashProjUmschalten(knopf){
const block = knopf.closest('.dash-proj');
if(!block) return;
const zu = !block.classList.contains('ist-zu');
block.classList.toggle('ist-zu', zu);
knopf.setAttribute('aria-expanded', String(!zu));
const set = dashZuLesen();
if(zu) set.add(block.dataset.key); else set.delete(block.dataset.key);
dashZuSchreiben(set);
dashAlleKnopfAktualisieren();
}
// dashAlleUmschalten: alle Projektblöcke auf einmal ein- bzw. ausklappen.
function dashAlleUmschalten(){
const bloecke = [...document.querySelectorAll('#dashboardContent .dash-proj')];
const zuklappen = bloecke.some(b => !b.classList.contains('ist-zu'));
const set = dashZuLesen();
bloecke.forEach(b => {
b.classList.toggle('ist-zu', zuklappen);
const k = b.querySelector('.dash-proj-toggle');
if(k) k.setAttribute('aria-expanded', String(!zuklappen));
if(zuklappen) set.add(b.dataset.key); else set.delete(b.dataset.key);
});
dashZuSchreiben(set);
dashAlleKnopfAktualisieren();
}
function dashAlleKnopfAktualisieren(){
const knopf = document.getElementById('dashAlleKnopf');
if(!knopf) return;
const offen = [...document.querySelectorAll('#dashboardContent .dash-proj')].some(b => !b.classList.contains('ist-zu'));
knopf.textContent = offen ? 'Alle einklappen' : 'Alle aufklappen';
}
// renderDashProjekte: die linke Zone. Parameter: daten (dashDaten()).
// Rückgabe: HTML-String.
function renderDashProjekte(daten){
if(!currentUser || !currentUser.personId) return dashLeer('Ohne zugeordnete Person gibt es keine eigenen Projekte.');
if(!daten.geladen) return dashLeer('Projekte werden geladen…');
if(!daten.projekte.length) return dashLeer('Sie sind in der aktuellen Sitzung keinem Projekt zugeordnet und haben keine offenen Aufgaben.');
const heute = heuteIso();
const zu = dashZuLesen();
return '
';
}
// 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();
}