Files
projektuebersicht/public/js/plan/plan-state.js
T

234 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// -----------------------------
// Projektplan — Zustand, Initialisierung & Daten laden
// -----------------------------
// Dieses Modul wird als erstes der plan-*-Module von public/project-plan.html
// geladen und definiert window.PlanApp als gemeinsamen Zustand aller
// Projektplan-Module. Es liest die URL-Route (Abteilung + Projektname, z. B.
// "/HLS/Projektplan/Gymnasium%20Hemer") und die URL-Parameter (weekLabel,
// projNr), über die das zu bearbeitende Projekt adressiert wird.
// Nach DOMContentLoaded werden Theme, Toolbar-Buttons und die Sitzungsdaten
// über die Server-API geladen. Die hier definierten Funktionen (savePlan,
// isLocked, showToast u. a.) werden über app.* den anderen plan-Modulen
// bereitgestellt, die alle in einem IIFE mit window.PlanApp laufen.
// PlanApp: gemeinsamer, modulübergreifender Zustand der Projektplan-Seite.
window.PlanApp = {
params: new URLSearchParams(location.search),
department: '',
weekLabel: '',
projNr: '',
projName: '',
week: null, // planWeek
project: null, // planProject
pxPerDay: 14, // Zoomstufe GANTT (null = an Containerbreite anpassen)
criticalPathActive: false, // Hervorhebung des kritischen Pfads im GANTT aktiv
// Drag-Zustände
todoDrag: null, // Drag-and-Drop von TODO/Meilenstein-Zeilen
ganttDrag: null, // Balken/Meilenstein/Urlaub verschieben
currentMilestoneIndex: null, // Index des gerade bearbeiteten Meilensteins (Modal)
currentTodoIndex: null, // Index der gerade bearbeiteten TODO (Modal)
currentAbsencePersonId: null, // Person-ID des offenen Abwesenheits-Modals
toastTimer: null, // Timer der Toast-Benachrichtigung (plan-state.js)
resizeTimer: null // Debounce-Timer für Fenster-Resize (plan-gantt.js)
};
(function(){
const app = window.PlanApp;
// Abteilung und Projektname aus dem Deep-Link-Pfad
// "/<Abteilung>/Projektplan/<Projektname>" lesen. Fallback auf die früheren
// Query-Parameter ?department=/&projName= für alte project-plan.html-Links.
const routeMatch = window.location.pathname.match(/^\/([^/]+)\/(?:Projektplan|project-plan\.html)(?:\/(.+))?\/?$/);
app.department = routeMatch ? routeMatch[1] : (app.params.get('department') || 'ELT');
app.weekLabel = app.params.get('weekLabel');
app.projNr = app.params.get('projNr') || '';
app.projName = routeMatch && routeMatch[2] ? decodeURIComponent(routeMatch[2]) : (app.params.get('projName') || '');
// init: Einstiegspunkt nach DOMContentLoaded. Übernimmt das Theme aus
// localStorage/System, prüft die URL-Parameter und bricht mit Fehlermeldung
// ab, falls kein Projekt ausgewählt wurde. Sonst bindet es die Toolbar und
// startet das Laden der Daten.
function init(){
loadTheme(); // Theme aus localStorage / System übernehmen
if(!app.weekLabel || !app.projName){
document.getElementById('planTitle').textContent = 'Fehler';
document.getElementById('planSubtitle').textContent = 'Kein Projekt ausgewählt.';
return;
}
currentDepartment = app.department; // global aus globals.js
bindToolbarButtons();
loadPlanData();
}
// bindToolbarButtons: verbindet die Toolbar-Buttons (TODO hinzufügen,
// Auto-Schedule, Kritischer Pfad) mit ihren globalen Funktionen der
// anderen plan-Module.
function bindToolbarButtons(){
const addTodoBtn = document.getElementById('addTodoBtn');
if(addTodoBtn) addTodoBtn.addEventListener('click', () => addTodo());
const autoScheduleBtn = document.getElementById('autoScheduleBtn');
if(autoScheduleBtn) autoScheduleBtn.addEventListener('click', () => autoScheduleTodos());
const criticalPathBtn = document.getElementById('criticalPathBtn');
if(criticalPathBtn) criticalPathBtn.addEventListener('click', () => toggleCriticalPath());
}
// loadPlanData: lädt alle Sitzungen und die globale Personenliste der
// Abteilung über die Server-API, sortiert die Wochen chronologisch und
// sucht daraus die Ziel-Woche bzw. das Ziel-Projekt. Zeigt Fehlermeldungen
// der Reihenfolge nach im Seitenkopf an und startet sonst renderPlanPage().
function loadPlanData(){
if(!window.fetch){
document.getElementById('planSubtitle').textContent = 'Netzwerk nicht verfügbar.';
return;
}
const url = baseUrl() + '/load-all?department=' + encodeURIComponent(app.department);
fetch(url)
.then(r => { if(!r.ok) throw new Error('Server HTTP '+r.status); return r.json(); })
.then(data => {
db = { weeks: [], currentWeek: null, globalPeople: {} };
db.globalPeople[app.department] = parseGlobalPeopleCsv(data.people);
const parsed = (data.sessions || []).map(s => ({ filename: s.filename, rows: parseCsvText(s.content) }));
parsed.forEach(s => {
const w = parseSessionCsv(s.filename, s.rows);
if(w) db.weeks.push(w);
});
db.weeks.sort((a,b) => weekSortKey(a.label).localeCompare(weekSortKey(b.label)));
app.week = db.weeks.find(w => w.label === app.weekLabel);
if(!app.week){
document.getElementById('planTitle').textContent = 'Nicht gefunden';
document.getElementById('planSubtitle').textContent = 'Sitzung "' + esc(app.weekLabel) + '" konnte nicht geladen werden.';
return;
}
app.project = app.week.projects.find(p => (p.nr || '') === app.projNr && (p.name || '') === app.projName);
if(!app.project){
document.getElementById('planTitle').textContent = 'Nicht gefunden';
document.getElementById('planSubtitle').textContent = 'Projekt nicht in dieser Sitzung vorhanden.';
return;
}
renderPlanPage();
})
.catch(err => {
document.getElementById('planTitle').textContent = 'Fehler beim Laden';
document.getElementById('planSubtitle').textContent = err.message;
});
}
// ensurePlanLocal: stellt sicher, dass das Plan-Objekt des Projekts die
// erwartete Struktur hat (inkl. Migration alter Einzel-Abhängigkeiten).
function ensurePlanLocal(){
if(!app.project.plan) app.project.plan = { start:'', end:'', todos:[], milestones:[], people:[], progress:0 };
if(!Array.isArray(app.project.plan.todos)) app.project.plan.todos = [];
// Migration alter Einzel-Abhängigkeiten zu Array
app.project.plan.todos.forEach(t => {
if(!Array.isArray(t.dependsOn)){
t.dependsOn = t.dependsOn ? [t.dependsOn] : [];
}
if(!Array.isArray(t.assignees)) t.assignees = [];
if(t.progress === undefined) t.progress = t.done ? 100 : 0;
});
return app.project.plan;
}
// isLocked: liefert true, wenn die aktuelle Woche gesperrt ist (dann sind
// Bearbeitungen im Plan gesperrt). Rückgabe: Boolescher Wert.
function isLocked(){
return !!app.week.locked;
}
// renderPlanPage: setzt Titel und Untertitel der Seite aus Projekt und Woche,
// blendet den Meilenstein-Button bei gesperrter Woche aus und startet die
// GANTT-Anzeige über fitPlanGanttRange().
function renderPlanPage(){
const p = app.project;
const plan = ensurePlanLocal();
document.getElementById('planTitle').textContent = (p.nr ? p.nr + ' – ' : '') + p.name;
document.getElementById('planSubtitle').textContent = app.week.label + (app.week.date ? ' · ' + formatWeekDate(app.week.date) : '');
// GANTT inkl. TODOs
const addMilestoneBtn = document.getElementById('addMilestoneBtn');
const locked = isLocked();
if(addMilestoneBtn) addMilestoneBtn.style.display = locked ? 'none' : '';
fitPlanGanttRange();
}
// Entfernt eine gelöschte ID aus allen dependsOn-Listen (TODOs und Meilensteine)
function removeDependencyRefs(plan, deletedId){
(plan.todos || []).forEach(t => {
if(Array.isArray(t.dependsOn)) t.dependsOn = t.dependsOn.filter(id => id !== deletedId);
});
(plan.milestones || []).forEach(m => {
if(Array.isArray(m.dependsOn)) m.dependsOn = m.dependsOn.filter(id => id !== deletedId);
});
}
// Beteiligte des Projekts (Haupt + Optional) mit E-Mail aus der globalen Liste
function renderProjectAssignees(){
const assignees = projectAssignees(app.project, app.week);
if(assignees.length === 0) return '<span class="hint">Keine Zuordnungen im Projektboard.</span>';
return assignees.map(a => {
const cls = a.state === 1 ? 'main' : a.state === 2 ? 'optional' : '';
return `<span class="todo-assignee ${cls}">${esc(a.person.name)}${a.role ? ' (' + esc(a.role) + ')' : ''}</span>`;
}).join('');
}
// Speichern: nur diese Sitzung; globale Personenliste nicht überschreiben
function savePlan(){
if(!app.week || !app.project) return;
const filename = filenameForWeek(app.week.label);
const csv = weekToCsv(app.week);
sendCsvToServer(filename, csv).catch(err => console.error('Speichern fehlgeschlagen:', err));
}
// saveGlobalPeople: schreibt die globale Personenliste der Abteilung als
// _people.csv auf den Server, aber nur wenn sie nicht leer ist.
function saveGlobalPeople(){
const people = db.globalPeople[app.department] || [];
if(people.length > 0){
const peopleCsv = globalPeopleToCsv(people);
sendCsvToServer('_people.csv', peopleCsv).catch(err => console.error('Speichern Personen fehlgeschlagen:', err));
}
}
// Person in der globalen Liste finden (dort werden Abwesenheiten gepflegt)
function findGlobalPerson(personId){
return (db.globalPeople[app.department] || []).find(p => p.id === personId) || null;
}
// countPersonAbsences: zählt die Abwesenheiten einer Person anhand der
// globalen Liste. Rückgabe: Anzahl (0 bei fehlender Person).
function countPersonAbsences(personId){
const p = findGlobalPerson(personId);
return p && Array.isArray(p.absences) ? p.absences.length : 0;
}
// Toast-Benachrichtigung
function showToast(message){
let toast = document.getElementById('planToast');
if(!toast){
toast = document.createElement('div');
toast.id = 'planToast';
toast.className = 'plan-toast';
document.body.appendChild(toast);
}
toast.textContent = message;
toast.classList.add('show');
// Vorherigen Timer abbrechen, sonst verschwindet ein neuer Toast zu früh
if(app.toastTimer) clearTimeout(app.toastTimer);
app.toastTimer = setTimeout(() => { toast.classList.remove('show'); app.toastTimer = null; }, 2500);
}
// Export der Hilfsfunktionen auf PlanApp, damit die anderen plan-Module
// (plan-drag, plan-modals, plan-schedule) sie direkt nutzen können.
app.ensurePlanLocal = ensurePlanLocal;
app.isLocked = isLocked;
app.removeDependencyRefs = removeDependencyRefs;
app.renderProjectAssignees = renderProjectAssignees;
app.savePlan = savePlan;
app.saveGlobalPeople = saveGlobalPeople;
app.findGlobalPerson = findGlobalPerson;
app.countPersonAbsences = countPersonAbsences;
app.showToast = showToast;
// Seitenstart: erst initialisieren, wenn das DOM geladen ist
document.addEventListener('DOMContentLoaded', init);
})();