Files
projektuebersicht/public/js/dashboard/dashboard.js
T
erikandClaude Opus 5.5 e12936444d Aufgaben im Wochenbericht, Meilensteine „Erreicht“, Panels als Planblatt
- Wochenbericht: Abschnitt „Aufgaben“ je Projekt (erledigt seit Vorwoche,
  überfällig, nächste zwei Wochen), rote Zahl in der Tagesordnung,
  Tabelle in der PDF-Akte; Klick öffnet das Aufgaben-Panel
- Meilensteine lassen sich als erreicht markieren (plan_milestones.erreicht);
  nur nicht erreichte vergangene gelten als überfällig
- Aufgabe- und Meilenstein-Panel neu aufgebaut: Stand oben, Lage im
  Terminplan, Angaben als Zeilen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 10:41:44 +01:00

853 lines
44 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// -----------------------------
// 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 = '<div class="hint">Lade Dashboard…</div>';
// 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 = '<div class="hint">Dashboard konnte nicht geladen werden: ' + esc(err.message || String(err)) + '</div>';
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 += '<div class="hint">Ihrem Benutzerkonto ist keine Person zugeordnet — es können daher keine Aufgaben angezeigt werden. Ein Administrator kann die Zuordnung in der Benutzerverwaltung ergänzen.</div>';
}
// Neu zugewiesene Aufgaben (js/benachrichtigungen.js) — vor allem anderen,
// weil es das ist, was sich seit dem letzten Blick geändert hat.
html += '<div id="dashNeu"></div>';
html += renderDashWoche(daten);
html += '<div class="dash-zonen">';
// Linke Zone: die eigenen Projekte mit ihren Terminen
html += '<div>';
const zuAlle = dashZuLesen();
const alleOffen = daten.projekte.some(x => !zuAlle.has(dashProjKey(x.p)));
html += dashAbschnitt('Meine Projekte', daten.projekte.length, null,
renderDashProjekte(daten),
daten.projekte.length > 1
? '<button type="button" class="dash-proj-alle" id="dashReihenfolgeReset" onclick="dashReihenfolgeZuruecksetzen()"'
+ (dashEigeneReihenfolge().length ? '' : ' hidden') + ' title="Reihenfolge wieder wie in der Projektliste">Wie Projektliste</button>'
+ '<button type="button" class="dash-proj-alle" id="dashAlleKnopf" onclick="dashAlleUmschalten()">'
+ (alleOffen ? 'Alle einklappen' : 'Alle aufklappen') + '</button>'
: '');
html += '</div>';
// Rechte Zone: Kalender, dann was man wissen muss, ohne es selbst zu tun
html += '<div>';
html += dashAbschnitt('Kalender', null, null, '<div class="dash-karte" id="dashKalender"></div>');
html += dashAbschnitt('Ausstehende Freigaben', pending.length, null,
pending.length === 0 ? dashLeer('Keine ausstehenden Freigaben.')
: '<div class="dash-karte">' + pending.map(renderDashboardFreigabe).join('') + '</div>');
html += dashAbschnitt('Abwesenheiten', absences.length, null,
renderDashboardAbsences(absences));
html += '</div>';
html += '</div>';
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 '<div class="dash-proj-liste">' + daten.projekte.map(x => {
const p = x.p;
const key = dashProjKey(p);
const istZu = zu.has(key);
const rollen = x.rollen.length
? x.rollen.map(r => esc(r.rolle) + (r.state === 2 ? ', Unterstützung' : '')).join(' / ')
: 'nur eigene Aufgabe';
const aktLp = (typeof lpActiveList === 'function' ? lpActiveList(p) : [])[0];
const pct = aktLp && p.lph && p.lph[aktLp] !== undefined && p.lph[aktLp] !== '' ? Math.max(0, Math.min(100, Number(p.lph[aktLp]) || 0)) : null;
const phase = aktLp
? '<span class="dash-proj-phase"><span>' + esc(aktLp) + '</span><span class="dash-auf-bar"><i style="width:' + (pct || 0) + '%"></i></span><span class="dash-auf-pct">' + (pct === null ? '—' : pct + '%') + '</span></span>'
: '<span class="dash-proj-phase is-leer">keine amtierende Leistungsphase</span>';
const ueber = x.termine.filter(t => t.datum && t.datum < heute);
const kommend = x.termine.filter(t => !t.datum || t.datum >= heute);
const alle = ueber.concat(kommend);
const sichtbar = alle.slice(0, DASH_TERMINE_JE_PROJEKT);
const rest = alle.length - sichtbar.length;
const zeilen = sichtbar.length
? sichtbar.map(t => renderDashTermin(p, t)).join('')
: '<div class="dash-leer">Keine offenen Termine.</div>';
const mehr = rest > 0
? '<a class="dash-proj-mehr" href="' + dashProjektUrl(p, 'aufgaben') + '" onclick="return dashImTabOeffnen(event, this)">' + rest + ' weitere Termine im Projekt</a>'
: '';
// Eingeklappt bleibt sichtbar, was Aufmerksamkeit braucht: wie viele
// Termine überfällig bzw. offen sind.
const kurz = (ueber.length ? '<span class="dash-proj-zahl ist-rot">' + ueber.length + ' überfällig</span>' : '')
+ '<span class="dash-proj-zahl' + (kommend.length ? ' ist-offen' : '') + '">' + kommend.length + ' offen</span>';
return '<div class="dash-karte dash-proj' + (istZu ? ' ist-zu' : '') + '" data-key="' + esc(key) + '">'
+ '<div class="dash-proj-kopf">'
+ (daten.projekte.length > 1 ? '<span class="dash-proj-griff" tabindex="0" role="button" title="Ziehen, um die Reihenfolge zu ändern (oder Pfeiltasten)" aria-label="Reihenfolge ändern" onmousedown="dashGriffDruck(this)" ontouchstart="dashGriffDruck(this)" onkeydown="dashGriffTaste(event, this)">' + icon('grip') + '</span>' : '')
+ '<button type="button" class="dash-proj-toggle" aria-expanded="' + (!istZu) + '" onclick="dashProjUmschalten(this)" title="Ein-/ausklappen">'
+ '<span class="dash-proj-chev" aria-hidden="true">›</span>'
+ '<span class="dash-proj-nr">' + esc(p.nr || '') + '</span><span class="dash-proj-titel">' + esc(p.name || '') + '</span>'
+ '</button>'
+ '<span class="dash-proj-kurz">' + kurz + '</span>'
+ '<a class="dash-proj-oeffnen" href="' + dashProjektUrl(p, 'dashboard') + '" onclick="return dashImTabOeffnen(event, this)" title="Projekt öffnen">Öffnen</a>'
+ '</div>'
+ '<div class="dash-proj-inhalt">'
+ renderDashProjektInfo(x, rollen)
+ '<div class="dash-proj-sub">' + phase + '</div>'
+ '<div class="dash-proj-termine">' + zeilen + mehr + '</div>'
+ '</div>'
+ '</div>';
}).join('') + '</div>';
}
// 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 += '<div class="dash-woche-tag' + (iso === heute ? ' ist-heute' : '') + '">'
+ '<div class="dash-woche-kopf"><span>' + TAGE[i] + '</span><span class="dash-woche-datum">' + d.getDate() + '.' + (d.getMonth() + 1) + '.</span></div>'
+ '<div class="dash-woche-liste">'
+ (sicht.map(x => {
const art = x.art === 'meilenstein' ? 'ist-ms' : (x.datum < heute ? 'ist-ueber' : (x.meins ? 'ist-meins' : 'ist-fremd'));
return '<a class="dash-woche-eintrag ' + art + '" href="' + dashProjektUrl(x.projekt, x.art === 'meilenstein' ? 'terminplan' : 'aufgaben') + '" onclick="return dashImTabOeffnen(event, this)"'
+ ' title="' + esc(x.titel + ' (' + [x.projekt.nr, x.projekt.name].filter(Boolean).join(' ') + ')') + '">'
+ '<span class="dash-woche-titel">' + esc(x.titel) + '</span>'
+ '<span class="dash-woche-projekt">' + esc([x.projekt.nr, x.projekt.name].filter(Boolean).join(' ')) + '</span></a>';
}).join('') || '<span class="dash-woche-leer">keine Termine</span>')
+ (rest > 0 ? '<span class="dash-woche-mehr">+ ' + rest + ' weitere</span>' : '')
+ '</div></div>';
}
const legende = '<div class="dash-woche-legende">'
+ '<span><i class="ist-meins"></i>meine Aufgabe</span><span><i class="ist-fremd"></i>Aufgabe im Projekt</span>'
+ '<span><i class="ist-ms"></i>Meilenstein</span><span><i class="ist-ueber"></i>überfällig</span></div>';
return dashAbschnitt('Diese Woche', null, 'eigene Aufgaben und Termine meiner Projekte',
'<div class="dash-karte dash-woche"><div class="dash-woche-rahmen"><div class="dash-woche-raster">' + spalten + '</div></div>' + legende + '</div>');
}
// renderDashProjektInfo: Zeile „Meine Rolle | Team | Notizen“ im Projektblock
// (Eriks Wunsch): die eigene Rolle, die übrigen Beteiligten als Kürzel-Kreise
// in den Rollenfarben (wie Projektliste, beteiligtenAvatarHtml()) und — nur
// wenn vorhanden — die Zahl der Notizen mit Sprung in den Reiter „Notizen“.
// Parameter: x (Eintrag aus dashDaten()), rollenText. Rückgabe: HTML-String.
const DASH_TEAM_MAX = 5;
function renderDashProjektInfo(x, rollenText){
const p = x.p;
const w = currentWeek();
const ich = currentUser && currentUser.personId;
const personen = {};
((w && w.people) || []).forEach(q => { personen[q.id] = q; });
const eintraege = [];
Object.keys(p.assign || {}).forEach(key => {
const [pid, role] = key.split('|');
const state = Number(p.assign[key]) || 0;
if(state > 0) eintraege.push({ person: personen[pid] || { id: pid, name: pid, fullName: pid }, role, state });
});
const alle = beteiligteJePerson(eintraege);
const ichB = alle.find(b => b.person.id === ich);
const team = alle.filter(b => b.person.id !== ich);
const kreis = b => beteiligtenAvatarHtml(b, typeof roleClass === 'function' ? roleClass(b.role) : '');
const kurzName = b => {
const teile = String(b.person.fullName || b.person.name || '').trim().split(/\s+/);
return teile.length > 1 ? teile[0] + ' ' + teile[teile.length - 1][0] + '.' : teile[0];
};
const namen = team.length > 2
? team.slice(0, 2).map(kurzName).join(', ') + ' +' + (team.length - 2)
: team.map(kurzName).join(', ');
let html = '<div class="dash-proj-info">'
+ '<div class="dash-proj-feld"><span class="dash-proj-feld-titel">Meine Rolle</span>'
+ '<span class="dash-proj-feld-wert">' + (ichB ? kreis(ichB) : '') + '<span>' + rollenText + '</span></span></div>'
+ '<div class="dash-proj-feld"><span class="dash-proj-feld-titel">Team</span>'
+ '<span class="dash-proj-feld-wert">'
+ (team.length
? '<span class="dash-proj-team">' + team.slice(0, DASH_TEAM_MAX).map(kreis).join('') + '</span><span class="dash-proj-namen" title="' + esc(team.map(b => b.title).join('\n')) + '">' + esc(namen) + '</span>'
: '<span class="dash-proj-namen">niemand weiter</span>')
+ '</span></div>';
const n = dashNotizen[p.projectUid];
if(n && n.anzahl > 0){
html += '<a class="dash-proj-feld dash-proj-notizen" href="' + dashProjektUrl(p, 'notizen') + '" onclick="return dashImTabOeffnen(event, this)">'
+ '<span class="dash-proj-feld-titel">Notizen</span>'
+ '<span class="dash-proj-feld-wert">' + icon('fileText') + '<span><b>' + n.anzahl + '</b> ' + (n.anzahl === 1 ? 'Notiz' : 'Notizen') + '</span>'
+ (n.zuletzt ? '<span class="dash-proj-zuletzt">' + esc(dashZuletzt(n.zuletzt)) + '</span>' : '') + '</span></a>';
}
return html + '</div>';
}
// dashZuletzt: „zuletzt heute/gestern“ oder „vom 28.10.“ zu einem Zeitstempel.
function dashZuletzt(zeit){
const d = new Date(zeit);
if(isNaN(d)) return '';
const tage = -tageBis(formatDateISO(d));
if(tage <= 0) return 'zuletzt heute';
if(tage === 1) return 'zuletzt gestern';
return 'vom ' + d.getDate() + '.' + (d.getMonth() + 1) + '.';
}
// renderDashTermin: eine Terminzeile im Projektblock und in der Tagesliste
// des Kalenders. Eigene Aufgaben tragen das Zeichen "A" in Akzentfarbe,
// fremde Aufgaben dazu die Zuständigen, Meilensteine eine Raute.
// Parameter: p (Projekt), t (Termin), mitProjekt (Projekt mit anzeigen).
// Rückgabe: HTML-String.
function renderDashTermin(p, t, mitProjekt){
const heute = heuteIso();
const ueber = t.art === 'aufgabe' && t.datum && t.datum < heute;
const bald = !ueber && t.datum && t.datum <= isoPlusTage(2);
const zeichen = t.art === 'meilenstein' ? '◆' : (t.meins ? 'A' : 'a');
const klasse = t.art === 'meilenstein' ? 'ist-ms' : (t.meins ? 'ist-meins' : 'ist-fremd');
const unter = mitProjekt
? (p.nr ? p.nr + ' ' : '') + (p.name || '')
: (t.art === 'meilenstein' ? 'Meilenstein' : (t.meins ? 'meine Aufgabe' : (t.wer.length ? t.wer.join(', ') : 'niemand zugewiesen')));
return '<a class="dash-termin" href="' + dashProjektUrl(p, t.art === 'meilenstein' ? 'terminplan' : 'aufgaben') + '" onclick="return dashImTabOeffnen(event, this)">'
+ '<span class="dash-termin-art ' + klasse + '">' + zeichen + '</span>'
+ '<span class="dash-termin-text"><span class="dash-termin-titel">' + esc(t.titel) + '</span>'
+ '<span class="dash-termin-meta">' + esc(unter) + '</span></span>'
+ '<span class="dash-termin-datum' + (ueber ? ' ist-ueberfaellig' : (bald ? ' ist-bald' : '')) + '">'
+ (t.datum ? esc(formatWeekDate(t.datum)) + '<em>' + esc(relativerTag(t.datum)) + '</em>' : 'ohne Frist')
+ '</span></a>';
}
// renderDashKalender: der Monatskalender der rechten Zone samt Tagesliste.
// Zeichnet nur #dashKalender neu (Blättern und Tageswahl lösen keinen neuen
// Abruf von /my-dashboard aus). Keine Parameter.
function renderDashKalender(){
const el = document.getElementById('dashKalender');
if(!el || !dashKalDaten) return;
const heute = heuteIso();
if(!dashKalTag) dashKalTag = heute;
if(!dashKalMonat){ const d = parseDateISO(heute); dashKalMonat = { jahr: d.getFullYear(), monat: d.getMonth() }; }
const { jahr, monat } = dashKalMonat;
const MONATE = ['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'];
// Einträge je Tag vorsortieren.
const jeTag = {};
dashKalDaten.eintraege.forEach(e => { (jeTag[e.datum] = jeTag[e.datum] || []).push(e); });
const abwesendAm = iso => dashKalDaten.abwesenheiten.filter(a => a.start <= iso && iso <= a.end);
const erster = new Date(jahr, monat, 1);
const start = new Date(erster); start.setDate(1 - ((erster.getDay() + 6) % 7));
let zellen = '';
for(let r = 0; r < 6; r++){
zellen += '<span class="dash-kal-kw">' + getWeekNumber(start) + '</span>';
for(let c = 0; c < 7; c++){
const iso = formatDateISO(start);
const e = jeTag[iso] || [];
const punkte = e.slice(0, 3).map(x => '<i class="' + (x.art === 'meilenstein' ? 'ist-ms'
: (x.datum < heute ? 'ist-rot' : (x.meins ? 'ist-meins' : 'ist-fremd'))) + '"></i>').join('')
+ (abwesendAm(iso).length ? '<i class="ist-ab"></i>' : '');
const feiertag = typeof HOLIDAYS !== 'undefined' && HOLIDAYS.has(iso);
const klassen = ['dash-kal-tag',
start.getMonth() !== monat ? 'ist-fremd' : '',
c > 4 || feiertag ? 'ist-frei' : '',
iso === heute ? 'ist-heute' : '',
iso === dashKalTag ? 'ist-gewaehlt' : ''].filter(Boolean).join(' ');
zellen += '<button type="button" class="' + klassen + '" onclick="dashKalWaehle(\'' + iso + '\')"'
+ ' aria-label="' + esc(formatWeekDate(iso)) + (e.length ? ', ' + e.length + ' Termine' : '') + '"'
+ (feiertag ? ' title="Feiertag"' : '') + '>'
+ start.getDate() + '<span class="dash-kal-punkte">' + punkte + '</span></button>';
start.setDate(start.getDate() + 1);
}
}
const tagE = (jeTag[dashKalTag] || []);
const tagA = abwesendAm(dashKalTag);
const feiertagHeute = typeof HOLIDAYS !== 'undefined' && HOLIDAYS.has(dashKalTag);
const tagD = parseDateISO(dashKalTag);
const tagTitel = tagD.toLocaleDateString('de-DE', { weekday: 'long', day: 'numeric', month: 'long' })
+ (dashKalTag === heute ? ' (heute)' : '');
const liste = (feiertagHeute ? '<div class="dash-kal-hinweis">Feiertag</div>' : '')
+ tagE.map(e => renderDashTermin(e.projekt, e, true)).join('')
+ tagA.map(a => '<div class="dash-termin"><span class="dash-termin-art ist-ab">–</span>'
+ '<span class="dash-termin-text"><span class="dash-termin-titel">' + esc(a.name) + '</span>'
+ '<span class="dash-termin-meta">' + esc(a.type) + ' bis ' + esc(formatWeekDate(a.end)) + '</span></span></div>').join('');
el.innerHTML = '<div class="dash-kal">'
+ '<div class="dash-kal-kopf">'
+ '<button type="button" class="dash-kal-nav" onclick="dashKalBlaettern(-1)" aria-label="Voriger Monat">‹</button>'
+ '<b>' + MONATE[monat] + ' ' + jahr + '</b>'
+ '<button type="button" class="dash-kal-nav" onclick="dashKalBlaettern(1)" aria-label="Nächster Monat">›</button>'
+ '</div>'
+ '<div class="dash-kal-raster"><span class="dash-kal-wt">KW</span>'
+ ['Mo','Di','Mi','Do','Fr','Sa','So'].map(t => '<span class="dash-kal-wt">' + t + '</span>').join('')
+ zellen + '</div>'
+ '<div class="dash-kal-legende"><span><i class="ist-meins"></i>meine Aufgabe</span><span><i class="ist-fremd"></i>Projektaufgabe</span>'
+ '<span><i class="ist-ms"></i>Meilenstein</span><span><i class="ist-rot"></i>überfällig</span><span><i class="ist-ab"></i>Abwesenheit</span></div>'
+ '</div>'
+ '<div class="dash-kal-liste"><div class="dash-kal-liste-titel">' + esc(tagTitel) + '</div>'
+ (liste || '<div class="dash-leer">Keine Termine an diesem Tag.</div>') + '</div>';
}
// 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: Titel und Kalenderwoche statt Begrüßung (Entwurf
// „Bauplan“, Eriks Wahl): „KW 45, Mo 2. bis Fr 6. November 2026“, rechts die
// Sitzung. Parameter: weekLabel. Rückgabe: HTML-String.
function renderDashboardKopf(weekLabel){
const MONATE = ['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'];
const mo = dashWochenMontag();
const fr = new Date(mo); fr.setDate(mo.getDate() + 4);
const zeile = 'KW ' + getWeekNumber(mo) + ', Mo ' + mo.getDate() + '.'
+ (mo.getMonth() !== fr.getMonth() ? ' ' + MONATE[mo.getMonth()] : '')
+ ' bis Fr ' + fr.getDate() + '. ' + MONATE[fr.getMonth()] + ' ' + fr.getFullYear();
return '<div class="dash-kopf">'
+ '<div class="dash-kopf-links"><span class="dash-kopf-kw">' + esc(zeile) + '</span>'
+ '<h2 class="dash-titel">Meine Aufgaben</h2></div>'
+ (weekLabel ? '<div class="dash-kopf-rechts"><span class="dash-kopf-label">Sitzung</span><span class="dash-sitzung">' + esc(weekLabel) + '</span></div>' : '')
+ '</div>';
}
// dashWochenMontag: Montag der laufenden Woche (lokale Zeit, 0 Uhr).
function dashWochenMontag(){
const d = parseDateISO(heuteIso());
d.setDate(d.getDate() - ((d.getDay() + 6) % 7));
return d;
}
// 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 = '<b class="ist-rot">' + z.ueberfaellig + '</b> '
+ (z.ueberfaellig === 1 ? 'Termin ist überfällig' : 'Termine sind überfällig')
+ (z.heuteFaellig ? ', <b>' + z.heuteFaellig + '</b> ' + (z.heuteFaellig === 1 ? 'wird' : 'werden') + ' heute fällig' : '')
+ '.';
} else if(z.heuteFaellig > 0){
satz = '<b>' + z.heuteFaellig + '</b> ' + (z.heuteFaellig === 1 ? 'Termin wird' : 'Termine werden')
+ ' heute fällig, nichts ist überfällig.';
} else if(z.aufgaben > 0){
satz = '<b class="ist-ruhig">Nichts überfällig.</b> ' + z.aufgaben + ' offene '
+ (z.aufgaben === 1 ? 'Aufgabe' : 'Aufgaben') + ' in Arbeit.';
} else {
satz = '<b class="ist-ruhig">Nichts offen, nichts überfällig.</b>';
}
const zahl = (wert, label, rot) =>
'<div class="dash-zahl' + (wert === 0 ? ' ist-null' : '') + (rot ? ' ist-rot' : '') + '"><span>' + label + '</span><b>' + wert + '</b></div>';
const ruhig = z.ueberfaellig === 0 && z.heuteFaellig === 0;
return '<div class="dash-band' + (ruhig ? ' ist-ruhig' : '') + '">'
+ '<p class="dash-befund">' + satz + '</p>'
+ '<div class="dash-band-zahlen">'
+ zahl(z.aufgaben, 'offene Aufgaben')
+ zahl(z.ueberfaellig, 'überfällig', z.ueberfaellig > 0)
+ zahl(z.dieseWoche, 'diese Woche fällig')
+ zahl(z.freigaben, 'Freigaben')
+ '</div></div>';
}
// 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 '<section class="dash-abschnitt">'
+ '<div class="dash-abschnitt-kopf">'
+ '<h3 class="dash-abschnitt-titel">' + esc(titel) + '</h3>'
+ (zahl != null ? '<span class="dash-abschnitt-zahl">' + zahl + '</span>' : '')
+ (zusatz ? '<span class="dash-abschnitt-zusatz">' + esc(zusatz) + '</span>' : '')
+ (kopfHtml || '')
+ '</div>' + inhalt + '</section>';
}
function dashLeer(text){
return '<div class="dash-karte"><div class="dash-leer">' + esc(text) + '</div></div>';
}
// 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 '<div class="dash-rand-zeile">'
+ '<span class="dash-avatar">' + esc(initialsFor(pr.uploadedBy || '?')) + '</span>'
+ '<span class="dash-rand-text">'
+ '<span class="dash-rand-name">' + esc(pr.originalFilename || '') + '</span>'
+ '<span class="dash-rand-meta">' + esc(projekt) + ' · ' + esc(pr.uploadedBy || '') + '</span>'
+ '</span></div>';
}
// 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
? '<span class="dash-chip ist-heute">heute</span>'
: '<span class="dash-chip ' + typKlasse + '">' + esc(a.type || 'Abwesend') + '</span>';
return '<div class="dash-rand-zeile">'
+ '<span class="dash-avatar">' + esc(initialsFor(a.name)) + '</span>'
+ '<span class="dash-rand-text">'
+ '<span class="dash-rand-name">' + esc(a.name || '') + '</span>'
+ '<span class="dash-rand-meta">' + esc(a.type || '') + ' · ' + esc(zeitraum)
+ (a.note ? ' · ' + esc(a.note) : '') + '</span>'
+ '</span>' + chip + '</div>';
}).join('');
return '<div class="dash-karte">' + zeilen + '</div>';
}
// 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();
}