code auf einzelne Datein aufgeteilt
This commit is contained in:
1 parent
7451e0bf78
commit
8c94fc91f6
30 files changed
+5765
-27
No files matched your search
@@ -0,0 +1,361 @@
|
||||
// -----------------------------
|
||||
// Projektübersicht — GANTT-Diagramm (gemeinsam genutzt)
|
||||
// -----------------------------
|
||||
// Gemeinsames Modul für das GANTT-Diagramm: Es wird von Test.html (nach
|
||||
// js/load.js) UND von project-plan.html geladen – dort zusammen mit globals,
|
||||
// utils, ui, date-picker, csv, server und den js/plan/*-Modulen, aber OHNE
|
||||
// js/store.js, js/weeks.js, js/board.js usw. Es darf daher auf der Plan-Seite
|
||||
// keine Hauptseiten-spezifischen Funktionen voraussetzen; Funktionen, die
|
||||
// currentWeek()/isWeekLocked() aus js/weeks.js brauchen (renderGantt,
|
||||
// fitGanttRange, openProjectPlan), werden nur vom Hauptfenster aufgerufen.
|
||||
// Das Modul stellt u. a. buildGanttHtml(), renderGantt(), fitGanttRange(),
|
||||
// projectAssignees(), ganttRangeFromProjects() sowie openProjectPlan()/
|
||||
// findProjectForPlan() bereit. Die Plan-Seite erweitert buildGanttHtml() über
|
||||
// js/plan/plan-build-gantt.js und bezieht Beteiligte/Range über
|
||||
// projectAssignees()/ganttRangeFromProjects() aus diesem Modul.
|
||||
|
||||
// ganttModalProjectId: reservierte Projekt-ID für GANTT-Modal-Dialoge (zurzeit ungenutzt).
|
||||
let ganttModalProjectId = null;
|
||||
|
||||
// ensurePlan: stellt sicher, dass ein Projekt ein gültiges Plan-Objekt besitzt
|
||||
// (Start/Ende, todos, Meilensteine, Personen, Fortschritt). Parameter: p =
|
||||
// Projektobjekt; Rückgabe: das Plan-Objekt von p.
|
||||
function ensurePlan(p){
|
||||
if(!p.plan) p.plan = { start:'', end:'', todos:[], milestones:[], people:[], progress:0 };
|
||||
// Alte Meilensteine bleiben erhalten; neu sind todos die primären Planungselemente.
|
||||
if(!Array.isArray(p.plan.todos)) p.plan.todos = [];
|
||||
if(!Array.isArray(p.plan.milestones)) p.plan.milestones = [];
|
||||
return p.plan;
|
||||
}
|
||||
|
||||
// Beteiligte eines Projekts aus dem Board ableiten (Haupt + Optional)
|
||||
// week optional: wenn die Funktion nicht im Kontext der aktuellen Hauptseite
|
||||
// aufgerufen wird (z.B. Projektplan-Seite), muss die passende Woche übergeben
|
||||
// werden, damit die Personenliste korrekt aufgelöst wird.
|
||||
function projectAssignees(p, week = null){
|
||||
const w = week || currentWeek();
|
||||
const people = w ? (w.people || []) : [];
|
||||
const globalList = db.globalPeople[currentDepartment] || [];
|
||||
const result = [];
|
||||
people.forEach(person => {
|
||||
const globalPerson = globalList.find(gp => gp.id === person.id);
|
||||
const email = globalPerson ? globalPerson.email : '';
|
||||
|
||||
(person.role || ['Planung']).forEach(role => {
|
||||
const key = assignKey(person.id, role);
|
||||
if(p.assign && p.assign[key]){
|
||||
result.push({ person, role, state: p.assign[key], email});
|
||||
}
|
||||
});
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// projectColor: wählt die Balkenfarbe eines Projekts aus GANTT_COLORS
|
||||
// deterministisch anhand der Projekt-ID. Parameter: p = Projektobjekt;
|
||||
// Rückgabe: Hex-Farbstring.
|
||||
function projectColor(p){
|
||||
const idx = Math.abs(String(p.id).split('').reduce((a,c)=>a+c.charCodeAt(0),0)) % GANTT_COLORS.length;
|
||||
return GANTT_COLORS[idx];
|
||||
}
|
||||
|
||||
// ganttRangeFromProjects: berechnet den sichtbaren Zeitraum aus allen
|
||||
// Projekt-/TODO-/Meilenstein-Terminen sowie den Abwesenheiten der globalen
|
||||
// Personen, jeweils erweitert um padDays Tage (Standard 7). Parameter:
|
||||
// projects = Projekt-Array, padDays = Puffer; Rückgabe: { start, end } als Date.
|
||||
function ganttRangeFromProjects(projects, padDays = 7){
|
||||
let min = null, max = null;
|
||||
projects.forEach(p => {
|
||||
const plan = p.plan;
|
||||
if(!plan) return;
|
||||
[plan.start, plan.end].forEach(d => {
|
||||
const date = parseDateISO(d);
|
||||
if(!date) return;
|
||||
if(!min || date < min) min = date;
|
||||
if(!max || date > max) max = date;
|
||||
});
|
||||
(plan.milestones || []).forEach(m => {
|
||||
const date = parseDateISO(m.date);
|
||||
if(!date) return;
|
||||
if(!min || date < min) min = date;
|
||||
if(!max || date > max) max = date;
|
||||
});
|
||||
(plan.todos || []).forEach(t => {
|
||||
[t.start, t.end].forEach(d => {
|
||||
const date = parseDateISO(d);
|
||||
if(!date) return;
|
||||
if(!min || date < min) min = date;
|
||||
if(!max || date > max) max = date;
|
||||
});
|
||||
});
|
||||
});
|
||||
// Urlaub berücksichtigen
|
||||
(db.globalPeople[currentDepartment] || []).forEach(person => {
|
||||
(person.absences || []).forEach(a => {
|
||||
const s = parseDateISO(a.start), e = parseDateISO(a.end);
|
||||
if(s){ if(!min || s < min) min = s; if(!max || s > max) max = s; }
|
||||
if(e){ if(!min || e < min) min = e; if(!max || e > max) max = e; }
|
||||
});
|
||||
});
|
||||
if(!min || !max){
|
||||
const today = new Date();
|
||||
min = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
max = addDays(min, 90);
|
||||
}
|
||||
return { start: addDays(min, -padDays), end: addDays(max, padDays) };
|
||||
}
|
||||
|
||||
// fitGanttRange: setzt die Datumsfelder (ganttStart/ganttEnd) auf den
|
||||
// automatisch berechneten Projekt-Zeitrahmen und rendert das Diagramm neu.
|
||||
function fitGanttRange(){
|
||||
const w = currentWeek(); if(!w) return;
|
||||
const range = ganttRangeFromProjects(w.projects, 7);
|
||||
document.getElementById('ganttStart').value = formatDateISO(range.start);
|
||||
document.getElementById('ganttEnd').value = formatDateISO(range.end);
|
||||
renderGantt();
|
||||
}
|
||||
|
||||
// renderGantt: rendert das GANTT-Diagramm der aktuellen Woche in
|
||||
// ganttContainer. Nutzt die Datumsfelder bzw. fällt auf den Projekt-Zeitrahmen
|
||||
// zurück; bei gesperrter Woche ist die Ansicht nicht editierbar.
|
||||
function renderGantt(){
|
||||
const w = currentWeek();
|
||||
const container = document.getElementById('ganttContainer');
|
||||
if(!w || !container){
|
||||
if(container) container.innerHTML = '<div class="hint">Keine Sitzung ausgewählt.</div>';
|
||||
return;
|
||||
}
|
||||
let start = parseDateISO(document.getElementById('ganttStart').value);
|
||||
let end = parseDateISO(document.getElementById('ganttEnd').value);
|
||||
if(!start || !end){
|
||||
const range = ganttRangeFromProjects(w.projects, 7);
|
||||
start = range.start; end = range.end;
|
||||
document.getElementById('ganttStart').value = formatDateISO(start);
|
||||
document.getElementById('ganttEnd').value = formatDateISO(end);
|
||||
}
|
||||
const showVacation = document.getElementById('ganttShowVacation').checked;
|
||||
container.innerHTML = buildGanttHtml(w.projects, start, end, { showVacation, editable: !isWeekLocked(w) });
|
||||
}
|
||||
|
||||
// buildGanttHtml: erzeugt das HTML des GANTT-Diagramms (Kopf mit Monaten/
|
||||
// KW-Zellen, Wochenend-Hintergrund, Heute-Linie, Urlaubs- und Projektzeilen
|
||||
// inkl. Fortschritt, Meilensteinen und Drag/Resize-Markern). Parameter:
|
||||
// projects = Projekt-Array, start/end = sichtbarer Zeitraum, options =
|
||||
// { showVacation, editable, highlightProjectId, pxPerDay, showTodos,
|
||||
// vacationPeople }. Rückgabe: HTML-String für .gantt-chart.
|
||||
function buildGanttHtml(projects, start, end, options = {}){
|
||||
const { showVacation = true, editable = false, highlightProjectId = null, pxPerDay = null } = options;
|
||||
const totalDays = Math.max(1, daysBetween(start, end) + 1);
|
||||
|
||||
// timeline: umhüllt den Zeileninhalt einer GANTT-Zeile mit der Tagesspuren-Containerklasse.
|
||||
function timeline(content){
|
||||
return `<div class="gantt-timeline">${content}</div>`;
|
||||
}
|
||||
|
||||
// dragAttrs: liefert die data-gantt-*-Attribute für Drag & Drop, leer wenn
|
||||
// nicht editierbar. type = Elementtyp, attrs = zusätzliche Attribute.
|
||||
function dragAttrs(type, attrs){
|
||||
if(!editable) return '';
|
||||
let s = ` data-gantt-type="${type}" draggable="false"`;
|
||||
for(const [k,v] of Object.entries(attrs)){
|
||||
s += ` data-gantt-${k}="${esc(String(v))}"`;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// resizeHandles: liefert die beiden Ziehgriffe links/rechts eines Balkens,
|
||||
// sofern die Ansicht editierbar ist.
|
||||
function resizeHandles(){
|
||||
if(!editable) return '';
|
||||
return `<span class="gantt-resize gantt-resize-left" data-resize="left"></span><span class="gantt-resize gantt-resize-right" data-resize="right"></span>`;
|
||||
}
|
||||
|
||||
// Kopfzeile: Monate und Tage
|
||||
let headerMonths = '<div class="gantt-row gantt-header">';
|
||||
headerMonths += '<div class="gantt-label">Projekt</div>';
|
||||
let monthsHtml = '';
|
||||
let current = new Date(start.getTime());
|
||||
let lastMonth = -1;
|
||||
let monthCells = [];
|
||||
for(let i = 0; i < totalDays; i++){
|
||||
if(current.getMonth() !== lastMonth){
|
||||
monthCells.push({ month: current.getMonth(), year: current.getFullYear(), startIdx: i });
|
||||
lastMonth = current.getMonth();
|
||||
}
|
||||
current = addDays(current, 1);
|
||||
}
|
||||
monthCells.forEach((m, idx) => {
|
||||
const next = monthCells[idx + 1];
|
||||
const span = next ? next.startIdx - m.startIdx : totalDays - m.startIdx;
|
||||
const names = ['Jan','Feb','Mär','Apr','Mai','Jun','Jul','Aug','Sep','Okt','Nov','Dez'];
|
||||
monthsHtml += `<div class="gantt-month" style="left:${(m.startIdx/totalDays*100).toFixed(4)}%;width:${(span/totalDays*100).toFixed(4)}%">${names[m.month]} ${m.year}</div>`;
|
||||
});
|
||||
headerMonths += timeline(monthsHtml) + '</div>';
|
||||
|
||||
let headerWeeks = '<div class="gantt-row gantt-days">';
|
||||
headerWeeks += '<div class="gantt-label"></div>';
|
||||
let weeksHtml = '';
|
||||
current = new Date(start.getTime());
|
||||
let lastWeek = -1;
|
||||
let lastYear = -1;
|
||||
let weekStartIdx = 0;
|
||||
const weekCells = [];
|
||||
for(let i = 0; i < totalDays; i++){
|
||||
const cw = getWeekNumber(current);
|
||||
const cy = current.getFullYear();
|
||||
if(cw !== lastWeek || cy !== lastYear){
|
||||
if(lastWeek !== -1){
|
||||
weekCells.push({ week: lastWeek, year: lastYear, startIdx: weekStartIdx, endIdx: i - 1 });
|
||||
}
|
||||
weekStartIdx = i;
|
||||
lastWeek = cw;
|
||||
lastYear = cy;
|
||||
}
|
||||
current = addDays(current, 1);
|
||||
}
|
||||
if(lastWeek !== -1){
|
||||
weekCells.push({ week: lastWeek, year: lastYear, startIdx: weekStartIdx, endIdx: totalDays - 1 });
|
||||
}
|
||||
weekCells.forEach(w => {
|
||||
const span = w.endIdx - w.startIdx + 1;
|
||||
weeksHtml += `<div class="gantt-day gantt-week" style="left:${(w.startIdx/totalDays*100).toFixed(4)}%;width:${(span/totalDays*100).toFixed(4)}%"><span>KW ${w.week}</span></div>`;
|
||||
});
|
||||
headerWeeks += timeline(weeksHtml) + '</div>';
|
||||
|
||||
// Raster-Overlay: Wochenenden schattieren und Heute-Linie markieren.
|
||||
// Liegt als eine Ebene hinter allen Zeilen (erste Chart-Kindnode),
|
||||
// damit nicht jede Zeile eigene Tageszellen braucht.
|
||||
let gridHtml = '<div class="gantt-grid-overlay">';
|
||||
current = new Date(start.getTime());
|
||||
let weekendStartIdx = null;
|
||||
for(let i = 0; i <= totalDays; i++){
|
||||
const isWeekend = i < totalDays && (current.getDay() === 0 || current.getDay() === 6);
|
||||
if(isWeekend && weekendStartIdx === null) weekendStartIdx = i;
|
||||
if(!isWeekend && weekendStartIdx !== null){
|
||||
const span = i - weekendStartIdx;
|
||||
gridHtml += `<div class="gantt-grid-weekend" style="left:${(weekendStartIdx/totalDays*100).toFixed(4)}%;width:${(span/totalDays*100).toFixed(4)}%"></div>`;
|
||||
weekendStartIdx = null;
|
||||
}
|
||||
current = addDays(current, 1);
|
||||
}
|
||||
const today = new Date();
|
||||
const todayMid = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
||||
if(todayMid >= start && todayMid <= end){
|
||||
const todayIdx = daysBetween(start, todayMid);
|
||||
gridHtml += `<div class="gantt-grid-today" style="left:${((todayIdx + 0.5)/totalDays*100).toFixed(4)}%" title="Heute: ${formatDateDE(todayMid)}"></div>`;
|
||||
}
|
||||
gridHtml += '</div>';
|
||||
|
||||
let body = gridHtml + headerMonths + headerWeeks;
|
||||
|
||||
// Urlaubszeilen
|
||||
if(showVacation){
|
||||
let allPeople = db.globalPeople[currentDepartment] || [];
|
||||
// Optional: nur Urlaub bestimmter Personen anzeigen
|
||||
if(options.vacationPeople && Array.isArray(options.vacationPeople)){
|
||||
const allowed = new Set(options.vacationPeople);
|
||||
allPeople = allPeople.filter(person => allowed.has(person.id));
|
||||
}
|
||||
// Pro Person eine Zeile: alle Abwesenheiten als Balken in derselben Timeline
|
||||
allPeople.forEach(person => {
|
||||
let barsHtml = '';
|
||||
(person.absences || []).forEach(a => {
|
||||
const s = parseDateISO(a.start), e = parseDateISO(a.end);
|
||||
if(!s || !e || e < start || s > end) return;
|
||||
const rowStart = s < start ? start : s;
|
||||
const rowEnd = e > end ? end : e;
|
||||
const offset = daysBetween(start, rowStart);
|
||||
const span = daysBetween(rowStart, rowEnd) + 1;
|
||||
barsHtml += `<div class="gantt-bar vacation"${dragAttrs('vacation',{person: person.id, absence: a.id, start: formatDateISO(s), end: formatDateISO(e)})} style="left:${(offset/totalDays*100).toFixed(4)}%;width:${(span/totalDays*100).toFixed(4)}%" title="${esc(a.type || 'Abwesenheit')}: ${formatDateDE(s)}–${formatDateDE(e)}${a.note ? ' – ' + esc(a.note) : ''}">${resizeHandles()}</div>`;
|
||||
});
|
||||
if(!barsHtml) return; // keine Abwesenheit im sichtbaren Zeitraum -> keine Zeile
|
||||
body += `<div class="gantt-row gantt-vacation">`;
|
||||
body += `<div class="gantt-label" title="${esc(person.fullName || person.name)}"><span class="gantt-vacation-dot"></span> ${esc(person.name)}</div>`;
|
||||
body += timeline(barsHtml);
|
||||
body += `</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
// Projektzeilen
|
||||
projects.slice().sort((a,b) => (a.order||0)-(b.order||0)).forEach(p => {
|
||||
const plan = p.plan;
|
||||
const color = projectColor(p);
|
||||
const milestones = plan && Array.isArray(plan.milestones) ? plan.milestones : [];
|
||||
body += `<div class="gantt-row gantt-project${highlightProjectId === p.id ? ' highlighted' : ''}">`;
|
||||
body += `<div class="gantt-label">${esc(p.nr ? p.nr + ' ' : '')}${esc(p.name)}</div>`;
|
||||
|
||||
let rowContent = '';
|
||||
|
||||
// Projekt-Balken
|
||||
const s = plan ? parseDateISO(plan.start) : null, e = plan ? parseDateISO(plan.end) : null;
|
||||
if(s && e){
|
||||
const barStart = s < start ? start : s;
|
||||
const barEnd = e > end ? end : e;
|
||||
if(barEnd >= start && barStart <= end){
|
||||
const offset = daysBetween(start, barStart);
|
||||
const span = daysBetween(barStart, barEnd) + 1;
|
||||
const progress = Math.max(0, Math.min(100, plan && parseFloat(plan.progress) ? parseFloat(plan.progress) : 0));
|
||||
rowContent += `<div class="gantt-bar project"${dragAttrs('project',{project: p.id, start: formatDateISO(s), end: formatDateISO(e)})} style="left:${(offset/totalDays*100).toFixed(4)}%;width:${(span/totalDays*100).toFixed(4)}%;background:${color}">`;
|
||||
rowContent += `${resizeHandles()}<div class="gantt-progress" style="width:${progress}%"></div>`;
|
||||
rowContent += `<span class="gantt-bar-label">${esc(p.name)}</span>`;
|
||||
rowContent += `</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Meilensteine: nur die nicht positionierten (keine position) in der Projektzeile anzeigen.
|
||||
// Positionierte Meilensteine werden vom Aufrufer (z.B. Projektplan) als separate Zeilen eingefügt.
|
||||
milestones.forEach((m, mIdx) => {
|
||||
if(options.showTodos && m.position) return;
|
||||
const d = parseDateISO(m.date);
|
||||
if(!d || d < start || d > end) return;
|
||||
const offset = daysBetween(start, d);
|
||||
rowContent += `<div class="gantt-milestone"${dragAttrs('milestone',{project: p.id, index: mIdx, date: formatDateISO(d)})} style="left:${(offset/totalDays*100).toFixed(4)}%" title="${esc(m.name)}: ${formatDateDE(d)}"></div>`;
|
||||
rowContent += `<div class="gantt-milestone-label" style="left:${(offset/totalDays*100).toFixed(4)}%">${esc(m.name || '')}</div>`;
|
||||
});
|
||||
|
||||
body += timeline(rowContent) + `</div>`;
|
||||
});
|
||||
|
||||
// pxPerDay setzt eine feste Breite pro Tag, damit das Diagramm bei hohem
|
||||
// Zoom breiter als der Container wird und horizontal gescrollt werden kann.
|
||||
let chartStyle = '';
|
||||
if(pxPerDay){
|
||||
chartStyle = ` style="min-width:${(220 + Math.ceil(totalDays * pxPerDay)).toFixed(0)}px"`;
|
||||
}
|
||||
return `<div class="gantt-chart"${chartStyle}>${body}</div>`;
|
||||
}
|
||||
|
||||
// ---------- Projektplan-Navigation ----------
|
||||
|
||||
// openProjectPlan: öffnet project-plan.html in einem neuen Tab. Weil
|
||||
// Projekt-IDs nicht persistent sind, werden department, Wochen-Label, Proj.-Nr.
|
||||
// und -Name als URL-Parameter übergeben. Parameter: pid = Projekt-ID der
|
||||
// aktuellen Woche. Kein Rückgabewert.
|
||||
function openProjectPlan(pid){
|
||||
const w = currentWeek(); if(!w) return;
|
||||
const p = w.projects.find(x => x.id === pid); if(!p) return;
|
||||
// IDs sind nicht persistent (werden bei jedem CSV-Laden neu erzeugt).
|
||||
// Daher werden stabile Werte (Label, Projekt-Nr., Name) in der URL übergeben.
|
||||
const url = new URL('project-plan.html', window.location.href);
|
||||
url.searchParams.set('department', currentDepartment);
|
||||
url.searchParams.set('weekLabel', w.label);
|
||||
url.searchParams.set('projNr', p.nr || '');
|
||||
url.searchParams.set('projName', p.name || '');
|
||||
window.open(url.toString(), '_blank');
|
||||
}
|
||||
|
||||
// Diese Funktion wird von project-plan.html aufgerufen, um das aktuelle
|
||||
// Wochen-/Projekt-Objekt im Hauptfenster zu finden.
|
||||
// Parameter: department, weekId, projectId; Rückgabe: { week, project } oder
|
||||
// null, wenn die Woche/Abteilung nicht (synchron) auflösbar ist.
|
||||
function findProjectForPlan(department, weekId, projectId){
|
||||
if(currentDepartment !== department){
|
||||
const targetWeek = db.weeks.find(w => w.id === weekId);
|
||||
if(!targetWeek) return null;
|
||||
// temporär umschalten ist unpraktisch, daher versuchen wir über load-all
|
||||
return null;
|
||||
}
|
||||
const w = db.weeks.find(x => x.id === weekId);
|
||||
if(!w) return null;
|
||||
return { week: w, project: w.projects.find(x => x.id === projectId) };
|
||||
}
|
||||
Reference in new issue
Block a user