- Gestaltung „Bauplan“: ruhigere Farben, Schriftskala, Abstände im 4-px-Raster, Kalenderwochen und Nummern in Mono-Ziffern, Glas beruhigt - Dashboard „Meine Aufgaben“: Kopf mit Kalenderwoche, Band „Diese Woche“ (Mo–Fr), je Projekt Meine Rolle / Team / Notizen; offene Termine blau hervorgehoben; Monatskalender unverändert - Neuer Bereich „Notizen“ in der Seitenleiste: Notizen aller eigenen Projekte und eigene Notizen ohne Projekt, später einem Projekt zuordenbar - Seitenleiste neu gegliedert (Projekte, Team, Notizen, Studenten, Einstellungen/Admin unten), Admin-Punkte in Personen/Vorgaben/System - Reiter im Projektkopf wie die Seitenleiste hervorgehoben - Behoben: „&amp;…“ in Kommentaren wuchs bei jedem Speichern; bestehende Kommentare werden beim Start repariert - Behoben: Lösch-Rückfrage bei Notizen unlesbar bzw. blieb stehen, Cursor im Notiztitel verdeckt, Dropdown im Dunkelmodus gekachelt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
853 lines
44 KiB
JavaScript
853 lines
44 KiB
JavaScript
// -----------------------------
|
||
// 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: [] });
|
||
});
|
||
const hatEigene = termine.some(t => t.meins);
|
||
if(!rollen.length && !hatEigene) return;
|
||
// Erledigte Vergangenheit interessiert hier nicht: Meilensteine vor heute
|
||
// fallen weg (sie haben kein Erledigt-Kennzeichen), offene Aufgaben vor
|
||
// heute bleiben als überfällig stehen.
|
||
const relevant = termine.filter(t => !(t.art === 'meilenstein' && t.datum < heute));
|
||
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();
|
||
}
|