193 lines
9.5 KiB
JavaScript
193 lines
9.5 KiB
JavaScript
// -----------------------------
|
|
// Projektbereich — Bereichs-Navigation & Dashboard
|
|
// -----------------------------
|
|
// Der Projektbereich ist der EIGENE BEREICH eines Projekts — seit dem Umbau
|
|
// kein eigenes Dokument mehr, sondern der Bereich #plan der Hauptseite
|
|
// Test.html. Er gliedert sich in sieben Unterbereiche, zwischen denen die
|
|
// Leiste .plan-nav umschaltet (Markup in Test.html):
|
|
//
|
|
// dashboard — Kennzahlen, Sitzungskommentar, Leistungsphasen-
|
|
// Fortschritt, nächste Termine, Team, Kompakt-Karten
|
|
// terminplan — GANTT mit Werkzeugleiste und Ressourcen-Auslastung
|
|
// aufgaben — Aufgaben- und Meilensteinliste (js/plan/plan-tasks.js)
|
|
// leistung — Leistungsstand & Abrechnung (js/plan/plan-leistung.js)
|
|
// dateien — Datei-Upload/-Freigabe (js/plan/plan-files.js)
|
|
// protokolle — Besprechungsprotokolle (js/plan/plan-protokoll.js)
|
|
// einstellungen — Projektangaben, Honorar, beauftragte Leistungsphasen,
|
|
// Beteiligte (js/plan/plan-settings.js)
|
|
//
|
|
// Alle Bereiche liegen gleichzeitig im DOM und werden nur über die Klasse
|
|
// .is-active ein- bzw. ausgeblendet. Das ist Absicht: die vorhandenen
|
|
// Render-Funktionen (renderPlanInfo, renderResourceLoad, renderProjectFiles …)
|
|
// befüllen ihre Elemente unabhängig davon, ob der Bereich gerade sichtbar ist,
|
|
// und brauchten für den Umbau keine Änderung.
|
|
//
|
|
// Der gewählte Bereich steht im URL-Fragment (z. B. …/Projektplan/Haus%20A
|
|
// ?weekLabel=…&projNr=…#aufgaben), damit ein Link auf einen bestimmten Bereich
|
|
// verschickt werden kann. Ein unbekanntes Fragment fällt auf "dashboard"
|
|
// zurück.
|
|
//
|
|
// Läuft wie alle plan-Module in einem IIFE; Zustand: window.PlanApp
|
|
// (plan-state.js).
|
|
|
|
(function(){
|
|
const app = window.PlanApp;
|
|
|
|
// PLAN_SECTIONS: gültige Bereichsschlüssel in der Reihenfolge der
|
|
// Navigationsleiste. Muss zu den data-section-Attributen in
|
|
// Test.html passen.
|
|
const PLAN_SECTIONS = ['dashboard', 'terminplan', '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
|
|
// beim Terminplan zwingend: die Abhängigkeitslinien im GANTT werden aus
|
|
// Pixel-Positionen berechnet, die in einem ausgeblendeten Container alle 0
|
|
// wären. Parameter: key (Bereichsschlüssel). Keine Rückgabe.
|
|
window.showPlanSection = function(key){
|
|
if(PLAN_SECTIONS.indexOf(key) === -1) key = 'dashboard';
|
|
app.section = key;
|
|
|
|
PLAN_SECTIONS.forEach(k => {
|
|
const sec = document.getElementById('planSection-' + k);
|
|
if(sec) sec.classList.toggle('is-active', k === key);
|
|
});
|
|
document.querySelectorAll('#planNav .plan-nav-item').forEach(btn => {
|
|
btn.classList.toggle('is-active', btn.dataset.section === key);
|
|
});
|
|
|
|
// Bereich im URL-Fragment festhalten (teilbarer Link), ohne einen
|
|
// History-Eintrag je Klick zu erzeugen.
|
|
try {
|
|
history.replaceState(null, '', location.pathname + location.search + '#' + key);
|
|
} catch(e) { /* z. B. file:// — dann bleibt die Adresse eben unverändert */ }
|
|
|
|
if(!app.project) return;
|
|
if(key === 'terminplan' && typeof renderPlanGantt === 'function') renderPlanGantt();
|
|
if(key === 'aufgaben' && typeof renderPlanTasks === 'function') renderPlanTasks();
|
|
if(key === 'leistung' && typeof renderPlanLeistung === 'function') renderPlanLeistung();
|
|
if(key === 'protokolle' && typeof renderPlanProtokolle === 'function') renderPlanProtokolle();
|
|
if(key === 'einstellungen' && typeof renderPlanSettings === 'function') renderPlanSettings();
|
|
if(key === 'dashboard' && typeof window.renderPlanDashboard === 'function') window.renderPlanDashboard();
|
|
};
|
|
|
|
// initPlanSections: wählt beim Seitenaufbau den Bereich aus dem
|
|
// URL-Fragment (sonst "dashboard"). Wird von renderPlanPage()
|
|
// (plan-state.js) aufgerufen, also erst wenn Woche und Projekt geladen
|
|
// sind. Keine Parameter, keine Rückgabe.
|
|
function initPlanSections(){
|
|
const fromHash = (location.hash || '').replace(/^#/, '');
|
|
// Ein Link auf #dateien/#leistung/#protokolle eines Sandbox-Plans (dessen
|
|
// Knöpfe ausgeblendet sind, siehe renderPlanPage(), plan-state.js) fiele
|
|
// sonst auf einen Bereich zurück, der zwar existiert, aber keinen
|
|
// sichtbaren Weg mehr dorthin hätte. Protokolle hängen wie Dateien an
|
|
// project.nr, den ein Sandbox-Plan nicht hat.
|
|
const gesperrtFuerSandbox = app.isSandboxPlan()
|
|
&& (fromHash === 'dateien' || fromHash === 'leistung' || fromHash === 'protokolle');
|
|
window.showPlanSection(!gesperrtFuerSandbox && PLAN_SECTIONS.indexOf(fromHash) !== -1 ? fromHash : 'dashboard');
|
|
}
|
|
|
|
// hidePlanNav: blendet die Bereichsleiste aus. Wird von plan-state.js
|
|
// gerufen, wenn gar kein Projekt geladen werden konnte (fehlende
|
|
// URL-Parameter, unbekannte Sitzung, Ladefehler) — dann führte jeder
|
|
// Bereich nur in eine leere Maske. Keine Parameter, keine Rückgabe.
|
|
function hidePlanNav(){
|
|
const nav = document.getElementById('planNav');
|
|
if(nav) nav.style.display = 'none';
|
|
}
|
|
|
|
// showPlanNav: Gegenstück zu hidePlanNav(). Nötig, seit der Projektbereich
|
|
// ein Bereich der Hauptseite ist und nacheinander mehrere Projekte zeigen
|
|
// kann: Nach einem Fehlschlag (hidePlanNav) muss die Leiste beim nächsten,
|
|
// erfolgreichen openPlanView() wieder erscheinen. Auf der früheren
|
|
// eigenständigen Seite war das unnötig, weil jeder Aufruf eine frische
|
|
// Seite lud. Keine Parameter, keine Rückgabe.
|
|
function showPlanNav(){
|
|
const nav = document.getElementById('planNav');
|
|
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 }.
|
|
function upcomingEntries(){
|
|
const plan = app.ensurePlanLocal();
|
|
const today = new Date();
|
|
today.setHours(0, 0, 0, 0);
|
|
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.milestones || []).forEach(m => {
|
|
const d = parseDateISO(m.date);
|
|
if(!d) return;
|
|
rows.push({ kind: 'Meilenstein', title: m.name || 'Meilenstein', date: d, overdue: d < today });
|
|
});
|
|
|
|
rows.sort((a, b) => a.date - b.date);
|
|
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.
|
|
window.renderPlanDashboard = function(){
|
|
if(!app.project) return;
|
|
|
|
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 teamEl = document.getElementById('planDashTeam');
|
|
if(teamEl){
|
|
const assignees = projectAssignees(app.project, app.week);
|
|
if(assignees.length === 0){
|
|
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>
|
|
</div>`;
|
|
}).join('') + '</div>';
|
|
}
|
|
}
|
|
};
|
|
|
|
// Wird das Fragment von außen geändert (Vor-/Zurück-Taste, von Hand
|
|
// getippter Anker, Link auf dieselbe Seite mit anderem Bereich), dann
|
|
// mitziehen. showPlanSection() selbst benutzt history.replaceState() und
|
|
// löst dieses Ereignis daher nicht aus.
|
|
window.addEventListener('hashchange', () => {
|
|
if(currentTab !== 'plan') return;
|
|
const key = (location.hash || '').replace(/^#/, '');
|
|
if(PLAN_SECTIONS.indexOf(key) !== -1 && key !== app.section) window.showPlanSection(key);
|
|
});
|
|
|
|
app.initPlanSections = initPlanSections;
|
|
app.hidePlanNav = hidePlanNav;
|
|
app.showPlanNav = showPlanNav;
|
|
})();
|