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
+115
-43
@@ -36,7 +36,7 @@
|
||||
// PLAN_SECTIONS: gültige Bereichsschlüssel in der Reihenfolge der
|
||||
// Navigationsleiste. Muss zu den data-section-Attributen in
|
||||
// App.html passen.
|
||||
const PLAN_SECTIONS = ['dashboard', 'terminplan', 'aufgaben', 'leistung', 'dateien', 'protokolle', 'einstellungen'];
|
||||
const PLAN_SECTIONS = ['dashboard', 'terminplan', 'phasen', 'aufgaben', 'leistung', 'dateien', 'protokolle', 'einstellungen'];
|
||||
|
||||
// showPlanSection: schaltet auf einen Bereich um, hebt den zugehörigen
|
||||
// Knopf hervor und rendert den Inhalt des Bereichs neu. Das Neu-Rendern ist
|
||||
@@ -63,6 +63,7 @@
|
||||
|
||||
if(!app.project) return;
|
||||
if(key === 'terminplan' && typeof renderPlanGantt === 'function') renderPlanGantt();
|
||||
if(key === 'phasen' && typeof renderPlanPhasen === 'function') renderPlanPhasen();
|
||||
if(key === 'aufgaben' && typeof renderPlanTasks === 'function') renderPlanTasks();
|
||||
if(key === 'leistung' && typeof renderPlanLeistung === 'function') renderPlanLeistung();
|
||||
if(key === 'protokolle' && typeof renderPlanProtokolle === 'function') renderPlanProtokolle();
|
||||
@@ -106,53 +107,88 @@
|
||||
if(nav) nav.style.display = '';
|
||||
}
|
||||
|
||||
// upcomingEntries: sammelt die nächsten Termine des Projekts — offene
|
||||
// Aufgaben mit Endtermin und Meilensteine mit Datum — in einer gemeinsamen,
|
||||
// chronologisch sortierten Liste. Überfällige Einträge stehen zuerst, damit
|
||||
// sie im Dashboard nicht untergehen. Rückgabe: Array von
|
||||
// { kind, title, date, overdue }.
|
||||
// upcomingEntries: offene Aufgaben mit Endtermin und Meilensteine ab heute
|
||||
// (vergangene Meilensteine haben kein Erledigt-Kennzeichen und gelten als
|
||||
// gewesen), chronologisch. idx verweist in plan.todos/plan.milestones, damit
|
||||
// ein Klick denselben Dialog öffnet wie im Terminplan.
|
||||
// Rückgabe: Array von { art, idx, titel, datum, iso, wer, meins }.
|
||||
function upcomingEntries(){
|
||||
const plan = app.ensurePlanLocal();
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const heute = heuteIso();
|
||||
const ich = currentUser && currentUser.personId;
|
||||
const namen = {};
|
||||
((app.week && app.week.people) || []).forEach(x => { namen[x.id] = x.fullName || x.name; });
|
||||
const rows = [];
|
||||
|
||||
(plan.todos || []).forEach(t => {
|
||||
if(t.done) return;
|
||||
const d = parseDateISO(t.end);
|
||||
if(!d) return;
|
||||
rows.push({ kind: 'Aufgabe', title: t.title || 'Aufgabe', date: d, overdue: d < today });
|
||||
(plan.todos || []).forEach((t, idx) => {
|
||||
if(t.done || !t.end) return;
|
||||
const wer = (t.assignees || []).map(id => namen[id] || id);
|
||||
rows.push({ art: 'aufgabe', idx, titel: t.title || 'Aufgabe', iso: t.end, wer,
|
||||
meins: !!ich && (t.assignees || []).includes(ich) });
|
||||
});
|
||||
(plan.milestones || []).forEach(m => {
|
||||
const d = parseDateISO(m.date);
|
||||
if(!d) return;
|
||||
rows.push({ kind: 'Meilenstein', title: m.name || 'Meilenstein', date: d, overdue: d < today });
|
||||
(plan.milestones || []).forEach((m, idx) => {
|
||||
if(!m.date || m.date < heute) return;
|
||||
rows.push({ art: 'meilenstein', idx, titel: m.name || 'Meilenstein', iso: m.date, wer: [], meins: false });
|
||||
});
|
||||
|
||||
rows.sort((a, b) => a.date - b.date);
|
||||
rows.sort((a, b) => a.iso.localeCompare(b.iso));
|
||||
return rows;
|
||||
}
|
||||
|
||||
// renderPlanDashboard: füllt die beiden eigenen Karten des Dashboards
|
||||
// ("Nächste Termine" und "Team"). Die übrigen Inhalte des Bereichs
|
||||
// stammen aus bereits vorhandenen Render-Funktionen: die KPI-Kacheln aus
|
||||
// renderPlanKpis() (plan-gantt.js), Kommentar und Leistungsphasen aus
|
||||
// renderPlanInfo() (plan-info.js), die Kompakt-Karten aus
|
||||
// renderResourceLoad()/renderProjectFiles()/renderPlanHistory().
|
||||
// Keine Parameter, keine Rückgabe.
|
||||
// PLAN_DASH_ANTEIL: wie viele Einträge je Gruppe offen dastehen — jede
|
||||
// Gruppe bekommt ihren eigenen Anteil, sonst füllten 25 überfällige
|
||||
// Aufgaben die Liste und "Diese Woche" stand ohne Einträge da. Der Rest
|
||||
// steckt hinter "… weitere in Aufgaben".
|
||||
const PLAN_DASH_ANTEIL = { ueber: 4, woche: 4, spaeter: 3 };
|
||||
|
||||
// renderPlanDashboard: füllt "Termine und Aufgaben" (gegliedert nach
|
||||
// Überfällig / Diese Woche / Später) und "Team" (eine Zeile je PERSON mit
|
||||
// ihren Rollen als Chips — vorher stand jemand mit drei Rollen dreimal da).
|
||||
// Kennzahlenband, Kommentar, Leistungsphasen und Unterlagen füllen
|
||||
// renderPlanKpis()/renderPlanInfo()/renderResourceLoad()/
|
||||
// renderProjectFiles(). Keine Parameter.
|
||||
window.renderPlanDashboard = function(){
|
||||
if(!app.project) return;
|
||||
const locked = app.isLocked();
|
||||
|
||||
const upcomingEl = document.getElementById('planDashUpcoming');
|
||||
if(upcomingEl){
|
||||
const rows = upcomingEntries().slice(0, 8);
|
||||
upcomingEl.innerHTML = rows.length === 0
|
||||
? '<div class="plan-task-empty">Keine offenen Termine hinterlegt.</div>'
|
||||
: '<div class="plan-upcoming-list">' + rows.map(r => `<div class="plan-upcoming-item${r.overdue ? ' overdue' : ''}">
|
||||
<span class="plan-upcoming-date">${esc(formatDateDE(r.date))}</span>
|
||||
<span>${esc(r.title)}</span>
|
||||
<span class="plan-upcoming-kind">${esc(r.kind)}</span>
|
||||
</div>`).join('') + '</div>';
|
||||
const heute = heuteIso();
|
||||
const wocheEnde = isoPlusTage(7);
|
||||
const alle = upcomingEntries();
|
||||
if(alle.length === 0){
|
||||
upcomingEl.innerHTML = '<div class="plan-task-empty">Keine offenen Termine. Aufgaben und Meilensteine entstehen im Terminplan.</div>';
|
||||
} else {
|
||||
const gruppen = [
|
||||
{ key: 'ueber', titel: 'Überfällig', klasse: 'ist-rot', treffer: r => r.art === 'aufgabe' && r.iso < heute },
|
||||
{ key: 'woche', titel: 'Diese Woche', klasse: '', treffer: r => r.iso >= heute && r.iso <= wocheEnde },
|
||||
{ key: 'spaeter', titel: 'Später', klasse: '', treffer: r => r.iso > wocheEnde },
|
||||
];
|
||||
let gezeigt = 0;
|
||||
let html = '';
|
||||
gruppen.forEach(g => {
|
||||
const alleDerGruppe = alle.filter(g.treffer);
|
||||
const sichtbar = alleDerGruppe.slice(0, PLAN_DASH_ANTEIL[g.key]);
|
||||
gezeigt += sichtbar.length;
|
||||
if(!alleDerGruppe.length) return;
|
||||
html += `<div class="plan-termin-gruppe ${g.klasse}">${esc(g.titel)}<span>${alleDerGruppe.length}</span></div>`;
|
||||
html += sichtbar.map(r => {
|
||||
const klick = locked ? '' : ` onclick="${r.art === 'meilenstein' ? 'openMilestoneModal' : 'openTodoModal'}(${r.idx})"`;
|
||||
const zeichen = r.art === 'meilenstein' ? '◆' : (r.meins ? 'A' : 'a');
|
||||
const klasse = r.art === 'meilenstein' ? 'ist-ms' : (r.meins ? 'ist-meins' : '');
|
||||
const wer = r.art === 'meilenstein' ? 'Meilenstein' : (r.wer.length ? r.wer.join(', ') : 'niemand zugewiesen');
|
||||
const rot = r.art === 'aufgabe' && r.iso < heute;
|
||||
return `<div class="plan-termin${locked ? '' : ' ist-klickbar'}"${klick}>
|
||||
<span class="plan-termin-art ${klasse}">${zeichen}</span>
|
||||
<span class="plan-termin-text"><b>${esc(r.titel)}</b><span>${esc(wer)}</span></span>
|
||||
<span class="plan-termin-datum${rot ? ' ist-rot' : ''}">${esc(formatDateDE(parseDateISO(r.iso)))}<em>${esc(relativerTag(r.iso))}</em></span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
});
|
||||
const verborgen = alle.length - gezeigt;
|
||||
if(verborgen > 0){
|
||||
html += `<button type="button" class="plan-panel-link plan-termin-mehr" onclick="showPlanSection('aufgaben')">${verborgen} weitere in „Aufgaben“</button>`;
|
||||
}
|
||||
upcomingEl.innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
const teamEl = document.getElementById('planDashTeam');
|
||||
@@ -162,14 +198,50 @@
|
||||
teamEl.innerHTML = '<div class="plan-task-empty">Diesem Projekt ist noch niemand zugeordnet. Zuordnung im Bereich „Einstellungen“.</div>';
|
||||
} else {
|
||||
const plan = app.ensurePlanLocal();
|
||||
teamEl.innerHTML = '<div class="plan-dash-team">' + assignees.map(a => {
|
||||
const offen = (plan.todos || []).filter(t => !t.done && Array.isArray(t.assignees) && t.assignees.indexOf(a.person.id) !== -1).length;
|
||||
const title = (a.person.fullName || a.person.name) + (a.role ? ' · ' + a.role : '');
|
||||
return `<div class="plan-dash-team-row">
|
||||
<span class="board-avatar ${app.planRoleClass(a.role)} state-${a.state}" title="${esc(title)}">${esc(a.person.name)}</span>
|
||||
<span>${esc(a.person.fullName || a.person.name)}</span>
|
||||
<span class="plan-upcoming-kind">${esc(a.role || '')}</span>
|
||||
<span class="plan-task-meta" style="margin-left:auto">${offen} offen</span>
|
||||
const jePerson = new Map();
|
||||
assignees.forEach(a => {
|
||||
if(!jePerson.has(a.person.id)) jePerson.set(a.person.id, { person: a.person, rollen: [] });
|
||||
jePerson.get(a.person.id).rollen.push({ rolle: a.role, state: a.state });
|
||||
});
|
||||
const zeile = e => {
|
||||
const offen = (plan.todos || []).filter(t => !t.done && Array.isArray(t.assignees) && t.assignees.indexOf(e.person.id) !== -1).length;
|
||||
const haupt = e.rollen.find(r => r.state === 1) || e.rollen[0];
|
||||
return `<div class="plan-team-zeile">
|
||||
<span class="board-avatar ${app.planRoleClass(haupt.rolle)} state-${haupt.state}">${esc(e.person.name)}</span>
|
||||
<span class="plan-team-text"><b>${esc(e.person.fullName || e.person.name)}</b>
|
||||
<span class="plan-team-rollen">${e.rollen.map(r => `<span class="plan-rolle${r.state === 2 ? ' ist-neben' : ''}" title="${r.state === 2 ? 'Unterstützung' : 'Hauptverantwortung'}">${esc(r.rolle || '')}</span>`).join('')}</span></span>
|
||||
<span class="plan-team-offen">${offen} offen</span>
|
||||
</div>`;
|
||||
};
|
||||
// Nach Gewerk gruppiert (Eriks Wahl "A"): erst die Gewerke des
|
||||
// Projekts in fester Reihenfolge — auch unbesetzte, mit Hinweis —,
|
||||
// dann Gewerke von Beteiligten, die nicht zum Projekt gehören, zuletzt
|
||||
// Personen ohne bekanntes Gewerk. Das Gewerk einer Person kommt aus
|
||||
// ihrem Konto (personGewerk(), users.department).
|
||||
const reihenfolge = Object.keys(DEPARTMENTS);
|
||||
const projGewerke = reihenfolge.filter(g => (app.project.gewerke || []).indexOf(g) !== -1);
|
||||
const gruppen = new Map(projGewerke.map(g => [g, []]));
|
||||
[...jePerson.values()].forEach(e => {
|
||||
const g = personGewerk(e.person) || '';
|
||||
if(!gruppen.has(g)) gruppen.set(g, []);
|
||||
gruppen.get(g).push(e);
|
||||
});
|
||||
const schluessel = [...gruppen.keys()].sort((x, y) => {
|
||||
const rang = g => g === '' ? 99 : (projGewerke.indexOf(g) !== -1 ? projGewerke.indexOf(g) : 10 + reihenfolge.indexOf(g));
|
||||
return rang(x) - rang(y);
|
||||
});
|
||||
teamEl.innerHTML = '<div class="plan-team">' + schluessel.map(g => {
|
||||
const leute = gruppen.get(g);
|
||||
const fremd = g !== '' && projGewerke.indexOf(g) === -1;
|
||||
const titel = g === '' ? 'Ohne Gewerk' : ((DEPARTMENTS[g] && DEPARTMENTS[g].label !== g) ? DEPARTMENTS[g].label : '');
|
||||
return `<div class="plan-team-gruppe">
|
||||
<div class="plan-team-gewerk">
|
||||
${g ? `<span class="plan-gewerk-schild">${esc(g)}</span>` : ''}
|
||||
<span class="plan-gewerk-titel">${esc(titel)}${fremd ? (titel ? ' · ' : '') + 'kein Gewerk des Projekts' : ''}</span>
|
||||
<span class="plan-gewerk-linie"></span>
|
||||
<span class="plan-gewerk-anzahl">${leute.length === 1 ? '1 Person' : leute.length + ' Personen'}</span>
|
||||
</div>
|
||||
${leute.length ? leute.map(zeile).join('') : '<div class="plan-team-luecke">Noch niemand aus diesem Gewerk zugeordnet.</div>'}
|
||||
</div>`;
|
||||
}).join('') + '</div>';
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user