Termin aus Mail, Rollen & Rechte, Abwesenheiten, Glas-Darstellung und Pi-Betrieb

- Termin aus Mail: Mailtext oder .eml per Drag & Drop/Strg+V auf Terminplan
  oder Aufgaben, KI schlägt Aufgaben vor, Prüfung im Aufgaben-Panel; neues
  Notizfeld je Aufgabe (plan_todos.notiz)
- Terminplan: Meilensteine ohne Aufgabenbezug je in eigener Zeile,
  "+ Neu"-Menü klappt neben schmaler Seitenleiste nach rechts, Leistungsphasen
- Rollen & Rechte, Abwesenheiten-Seite, Benachrichtigungen, Rundgang
- Glas-Darstellung, Logo als Favicon
- Pi-Betrieb: PDF-Arbeit im Kindprozess, Wächter, Einrichtungs- und
  Prüfskript inkl. WLAN-Wächter

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
erikandClaude Opus 5.5 committed 2026-10-29 14:16:52 +01:00
1 parent f4fb3c550b
commit b7e0bd0d92
56 files changed
+12265 -2974

No files matched your search

+357 -124
View File
@@ -13,14 +13,22 @@
// 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.
// 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
@@ -109,20 +117,27 @@ async function renderDashboard(){
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 += '<div class="dash-zonen">';
// Linke Zone: der eigene Arbeitsstrom
// Linke Zone: die eigenen Projekte mit ihren Terminen
const daten = dashDaten();
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));
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="dashAlleKnopf" onclick="dashAlleUmschalten()">'
+ (alleOffen ? 'Alle einklappen' : 'Alle aufklappen') + '</button>'
: '');
html += '</div>';
// Rechte Zone: was man wissen muss, ohne es selbst zu tun
// 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>');
@@ -133,9 +148,331 @@ async function renderDashboard(){
html += '</div>';
container.innerHTML = html;
if(typeof renderDashNeu === 'function'){ renderDashNeu(); ladeBenachrichtigungen(true); }
dashKalDaten = daten;
renderDashKalender();
startDashboardClock();
}
// ---------- 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
// 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 });
});
// Projekte mit dem dringendsten Termin zuerst, Projekte ohne Termin am Ende.
projekte.sort((a, b) => {
const da = (a.termine.find(t => t.datum) || {}).datum || '9999';
const db_ = (b.termine.find(t => t.datum) || {}).datum || '9999';
return da.localeCompare(db_) || String(a.p.nr || '').localeCompare(String(b.p.nr || ''));
});
// 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();
}
// 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; }
// 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 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 => '<span class="dash-rolle' + (r.state === 2 ? ' ist-neben' : '') + '" title="'
+ (r.state === 2 ? 'Unterstützung' : 'Hauptverantwortung') + '">' + esc(r.rolle) + '</span>').join('')
: '<span class="dash-rolle ist-neben" title="Nicht zugeordnet, aber mit eigener Aufgabe">nur Aufgabe</span>';
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') + '" target="_blank" rel="noopener">' + 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 + ' offen</span>';
return '<div class="dash-karte dash-proj' + (istZu ? ' ist-zu' : '') + '" data-key="' + esc(key) + '">'
+ '<div class="dash-proj-kopf">'
+ '<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>'
+ '<span class="dash-rollen">' + rollen + '</span>'
+ '<a class="dash-proj-oeffnen" href="' + dashProjektUrl(p, 'dashboard') + '" target="_blank" rel="noopener" title="Projekt öffnen">Öffnen</a>'
+ '</div>'
+ '<div class="dash-proj-inhalt">'
+ '<div class="dash-proj-sub">' + phase + '</div>'
+ '<div class="dash-proj-termine">' + zeilen + mehr + '</div>'
+ '</div>'
+ '</div>';
}).join('');
}
// 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') + '" target="_blank" rel="noopener">'
+ '<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: 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.
@@ -194,12 +531,15 @@ function renderDashboardBand(z){
// 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){
// 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){
@@ -243,46 +583,6 @@ function startDashboardClock(){
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.
@@ -296,73 +596,6 @@ function relativerTag(iso){
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){