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:
1 parent
f4fb3c550b
commit
b7e0bd0d92
56 files changed
+12265
-2974
No files matched your search
+357
-124
@@ -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){
|
||||
|
||||
Reference in new issue
Block a user