All new
alle new
This commit is contained in:
1 parent
da64472251
commit
5b20adee48
1605 files changed
+396517
-1674
No files matched your search
@@ -0,0 +1,425 @@
|
||||
// -----------------------------
|
||||
// Projektübersicht — Dashboard ("Meine Aufgaben"-Tab)
|
||||
// -----------------------------
|
||||
// Modul für den ersten Tab der Hauptseite (Test.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 Der eigene Arbeitsstrom: "Meine Aufgaben" (persönlich
|
||||
// zugewiesene offene TODOs, als Zeilen —
|
||||
// renderDashboardAufgabe()) und "Termine meiner Projekte"
|
||||
// (Aufgaben und Meilensteine der Projekte, denen man zugeordnet
|
||||
// ist, nach Dringlichkeit gegliedert — renderDashboardFristen()).
|
||||
// 4. RECHTS Was man wissen muss, ohne selbst tätig zu werden: ausstehende
|
||||
// Freigaben und Abwesenheiten der Kollegen, beide in derselben
|
||||
// schmalen Zeilenform.
|
||||
//
|
||||
// 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 = '<div class="hint">Lade Dashboard…</div>';
|
||||
|
||||
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 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 += '<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>';
|
||||
}
|
||||
|
||||
html += '<div class="dash-zonen">';
|
||||
|
||||
// Linke Zone: der eigene Arbeitsstrom
|
||||
html += '<div>';
|
||||
html += dashAbschnitt('Meine Aufgaben', tasks.length, 'mir persönlich zugewiesen',
|
||||
!weekLabel ? dashLeer('Keine Sitzung vorhanden.')
|
||||
: (tasks.length === 0 ? dashLeer('Keine offenen Aufgaben — alles abgearbeitet.')
|
||||
: '<div class="dash-karte">' + tasks.map(t => renderDashboardAufgabe(weekLabel, t)).join('') + '</div>'));
|
||||
html += dashAbschnitt('Termine meiner Projekte', projectDates.length, 'nächste drei Wochen',
|
||||
renderDashboardFristen(weekLabel, projectDates));
|
||||
html += '</div>';
|
||||
|
||||
// Rechte Zone: was man wissen muss, ohne es selbst zu tun
|
||||
html += '<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;
|
||||
startDashboardClock();
|
||||
}
|
||||
|
||||
// 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 '<div class="dash-kopf">'
|
||||
+ '<p class="dash-gruss"><span id="dashboardGreetingText"></span>'
|
||||
+ '<em id="dashboardGreetingDate"></em></p>'
|
||||
+ '<div class="dash-kopf-rechts">'
|
||||
+ (weekLabel ? '<span class="dash-sitzung">' + esc(weekLabel) + '</span>' : '')
|
||||
+ '<span class="dash-uhr" id="dashboardClock"></span>'
|
||||
+ '</div>'
|
||||
+ '</div>';
|
||||
}
|
||||
|
||||
// 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) =>
|
||||
'<div class="dash-zahl' + (wert === 0 ? ' ist-null' : '') + '"><b>' + wert + '</b><span>' + label + '</span></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.termine, 'Termine im Blick')
|
||||
+ zahl(z.freigaben, 'Freigaben')
|
||||
+ zahl(z.abwesend, 'heute abwesend')
|
||||
+ '</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.
|
||||
function dashAbschnitt(titel, zahl, zusatz, inhalt){
|
||||
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>' : '')
|
||||
+ '</div>' + inhalt + '</section>';
|
||||
}
|
||||
function dashLeer(text){
|
||||
return '<div class="dash-karte"><div class="dash-leer">' + esc(text) + '</div></div>';
|
||||
}
|
||||
|
||||
// greetingForHour:// greetingForHour: liefert je Tageszeit eine passende Begrüßung. Parameter:
|
||||
// hour (Number, 0-23). Rückgabe: String.
|
||||
function greetingForHour(hour){
|
||||
if(hour < 5) return 'Gute Nacht';
|
||||
if(hour < 11) return 'Guten Morgen';
|
||||
if(hour < 18) return 'Guten Tag';
|
||||
if(hour < 22) return 'Guten Abend';
|
||||
return 'Gute Nacht';
|
||||
}
|
||||
|
||||
// startDashboardClock: (re-)startet das Intervall, das Begrüßung, Datum und
|
||||
// Uhrzeit im Dashboard-Header aktualisiert. Bricht sich selbst ab, sobald die
|
||||
// Zielelemente nicht mehr im DOM sind (z. B. weil das Dashboard neu gerendert
|
||||
// wurde). Keine Parameter, keine Rückgabe.
|
||||
function startDashboardClock(){
|
||||
if(dashboardClockTimer) clearInterval(dashboardClockTimer);
|
||||
const name = (currentUser && (currentUser.displayName || currentUser.username)) || '';
|
||||
const tick = () => {
|
||||
const textEl = document.getElementById('dashboardGreetingText');
|
||||
const dateEl = document.getElementById('dashboardGreetingDate');
|
||||
const clockEl = document.getElementById('dashboardClock');
|
||||
if(!textEl || !dateEl || !clockEl){
|
||||
clearInterval(dashboardClockTimer);
|
||||
dashboardClockTimer = null;
|
||||
return;
|
||||
}
|
||||
const now = new Date();
|
||||
// Begrüßung und Datum stehen in EINER Zeile (früher untereinander), das
|
||||
// Datum als gedämpfter Zusatz — deshalb der führende Gedankenstrich.
|
||||
textEl.textContent = greetingForHour(now.getHours()) + (name ? ', ' + name : '');
|
||||
dateEl.textContent = ' — ' + now.toLocaleDateString('de-DE', { weekday: 'long', day: '2-digit', month: 'long', year: 'numeric' });
|
||||
clockEl.textContent = now.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
||||
};
|
||||
tick();
|
||||
dashboardClockTimer = setInterval(tick, 30000);
|
||||
}
|
||||
|
||||
// renderDashboardAufgabe: eine eigene Aufgabe als ZEILE — Titel und Projekt
|
||||
// links, Fortschritt in der Mitte, Fälligkeit rechts.
|
||||
//
|
||||
// Zuvor war das eine Karte, die eine halbe Bildschirmbreite für vier Zeilen
|
||||
// Inhalt belegte; zwei davon füllten den sichtbaren Bereich. Dieselben
|
||||
// Angaben stehen jetzt in einer Zeile, acht davon auf derselben Fläche.
|
||||
// Verlinkt wie bisher auf den Projektbereich (gleiches URL-Schema wie
|
||||
// openProjectPlan() in js/gantt.js).
|
||||
// Parameter: weekLabel, task. Rückgabe: HTML-String.
|
||||
function renderDashboardAufgabe(weekLabel, task){
|
||||
const url = new URL('/' + PLAN_SLUG + '/' + encodeURIComponent(task.projectName || ''), window.location.origin);
|
||||
url.searchParams.set('weekLabel', weekLabel);
|
||||
url.searchParams.set('projNr', task.projectNr || '');
|
||||
url.hash = 'aufgaben';
|
||||
|
||||
const heute = heuteIso();
|
||||
const bald = isoPlusTage(2);
|
||||
// Dieselbe Kennzeichnung wie in der Terminliste darunter, damit die Kachel
|
||||
// "überfällig" und die beiden Listen zusammenpassen.
|
||||
const klasse = !task.end ? '' : (task.end < heute ? ' ist-ueberfaellig' : (task.end <= bald ? ' ist-bald' : ''));
|
||||
const datum = task.end ? formatWeekDate(task.end) : '—';
|
||||
const relativ = task.end ? relativerTag(task.end) : 'ohne Frist';
|
||||
const progress = Number(task.progress || 0);
|
||||
const tags = (Array.isArray(task.tags) ? task.tags : [])
|
||||
.map(t => '<span class="dash-auf-tag">' + esc(t) + '</span>').join('');
|
||||
const projekt = (task.projectNr ? task.projectNr + ' ' : '') + (task.projectName || '');
|
||||
|
||||
return '<a class="dash-auf" href="' + url.toString() + '" target="_blank" rel="noopener">'
|
||||
+ '<span class="dash-auf-text">'
|
||||
+ '<span class="dash-auf-titel">' + esc(task.title || '(ohne Titel)') + '</span>'
|
||||
+ '<span class="dash-auf-meta">' + esc(projekt) + tags + '</span>'
|
||||
+ '</span>'
|
||||
+ '<span class="dash-auf-fortschritt">'
|
||||
+ '<span class="dash-auf-bar"><i style="width:' + Math.min(100, Math.max(0, progress)) + '%"></i></span>'
|
||||
+ '<span class="dash-auf-pct">' + progress + '%</span>'
|
||||
+ '</span>'
|
||||
+ '<span class="dash-auf-frist' + klasse + '">' + esc(datum) + '<em>' + esc(relativ) + '</em></span>'
|
||||
+ '</a>';
|
||||
}
|
||||
|
||||
// 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';
|
||||
}
|
||||
|
||||
// renderDashboardFristen:// renderDashboardFristen: die Termine der Projekte, denen man zugeordnet ist
|
||||
// (GET /my-dashboard, Feld projectDates — bereits nach Datum sortiert, auf
|
||||
// 21 Tage begrenzt und um die eigenen Aufgaben bereinigt).
|
||||
//
|
||||
// Bewusst eine LISTE und keine Kartenwand wie "Meine Aufgaben": Hier geht es
|
||||
// um Reihenfolge in der Zeit, und Termine, die untereinander stehen, lassen
|
||||
// sich der Reihe nach abarbeiten. Gegliedert wird nach Dringlichkeit statt
|
||||
// nach Projekt — in der Woche, in der etwas fällig wird, ist das Datum die
|
||||
// Frage, nicht das Projekt.
|
||||
// Parameter: weekLabel, eintraege (Array). Rückgabe: HTML-String.
|
||||
function renderDashboardFristen(weekLabel, eintraege){
|
||||
if(!weekLabel) return dashLeer('Keine Sitzung vorhanden.');
|
||||
if(!eintraege.length){
|
||||
return dashLeer('In Ihren Projekten steht in den nächsten drei Wochen kein Termin an.');
|
||||
}
|
||||
|
||||
const wocheEnde = isoPlusTage(7);
|
||||
const gruppen = [
|
||||
{ titel: 'Überfällig', klasse: 'ist-ueberfaellig', treffer: d => d.ueberfaellig },
|
||||
{ titel: 'Diese Woche', klasse: 'ist-bald', treffer: d => !d.ueberfaellig && d.datum <= wocheEnde },
|
||||
{ titel: 'Danach', klasse: '', treffer: d => !d.ueberfaellig && d.datum > wocheEnde },
|
||||
];
|
||||
|
||||
// In derselben Karte wie "Meine Aufgaben" darüber: Beides ist der eigene
|
||||
// Arbeitsstrom und soll gleich aussehen.
|
||||
let html = '<div class="dash-karte">';
|
||||
gruppen.forEach(g => {
|
||||
const teil = eintraege.filter(g.treffer);
|
||||
if(!teil.length) return;
|
||||
html += '<div class="fristen-gruppe ' + g.klasse + '">'
|
||||
+ '<div class="fristen-gruppe-titel">' + esc(g.titel) + '<span>' + teil.length + '</span></div>';
|
||||
teil.forEach(d => { html += renderFristenZeile(weekLabel, d); });
|
||||
html += '</div>';
|
||||
});
|
||||
return html + '</div>';
|
||||
}
|
||||
|
||||
// renderFristenZeile: eine Zeile der Terminliste. Der ganze Eintrag führt in
|
||||
// den Terminplan des Projekts — dorthin, wo sich der Termin ändern lässt.
|
||||
// Parameter: weekLabel, d (Eintrag). Rückgabe: HTML-String.
|
||||
function renderFristenZeile(weekLabel, d){
|
||||
const url = new URL('/' + PLAN_SLUG + '/' + encodeURIComponent(d.projectName || ''), window.location.origin);
|
||||
url.searchParams.set('weekLabel', weekLabel);
|
||||
url.searchParams.set('projNr', d.projectNr || '');
|
||||
url.hash = 'aufgaben';
|
||||
|
||||
const datum = d.datum ? formatWeekDate(d.datum) : '';
|
||||
const tage = tageBis(d.datum);
|
||||
// Die Angabe in Tagen steht neben dem Datum, weil "12.08." allein nicht
|
||||
// sagt, ob das morgen oder in drei Wochen ist.
|
||||
const relativ = d.heute ? 'heute'
|
||||
: (tage < 0 ? (Math.abs(tage) === 1 ? 'seit 1 Tag' : 'seit ' + Math.abs(tage) + ' Tagen')
|
||||
: (tage === 1 ? 'morgen' : 'in ' + tage + ' Tagen'));
|
||||
const wer = Array.isArray(d.zustaendig) && d.zustaendig.length
|
||||
? esc(d.zustaendig.join(', '))
|
||||
: '<span class="hint" style="margin:0">niemand zugewiesen</span>';
|
||||
|
||||
return '<a class="fristen-zeile" href="' + url.toString() + '" target="_blank" rel="noopener">'
|
||||
+ '<span class="fristen-datum"><b>' + esc(datum) + '</b><em>' + esc(relativ) + '</em></span>'
|
||||
+ '<span class="fristen-art' + (d.art === 'meilenstein' ? ' ist-meilenstein' : '') + '">'
|
||||
+ (d.art === 'meilenstein' ? 'Meilenstein' : 'Aufgabe') + '</span>'
|
||||
+ '<span class="fristen-titel">' + esc(d.titel || '(ohne Titel)') + '</span>'
|
||||
+ '<span class="fristen-projekt">' + esc(d.projectNr ? d.projectNr + ' ' + d.projectName : d.projectName) + '</span>'
|
||||
+ '<span class="fristen-wer">' + wer + '</span>'
|
||||
+ '</a>';
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
Reference in new issue
Block a user