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
@@ -86,7 +86,7 @@
|
||||
// aufgerufen. Mit showTodos wird deren HTML per DOM-Bearbeitung um die
|
||||
// TODO- und Meilensteinzeilen aller übergebenen Projekte ergänzt und als
|
||||
// HTML-String zurückgegeben. Parameter: projects (Array), start/end (ISO-
|
||||
// Datum), options (Optionen inkl. editable und criticalPath). Rückgabe:
|
||||
// Datum), options (Optionen inkl. editable). Rückgabe:
|
||||
// String mit dem kompletten GANTT-HTML.
|
||||
window.buildGanttHtml = function(projects, start, end, options = {}){
|
||||
if(!options.showTodos) return originalBuildGanttHtml(projects, start, end, options);
|
||||
@@ -169,12 +169,11 @@
|
||||
const span = daysBetween(barStart, barEnd) + 1;
|
||||
const dragAttrs = editable ? ` data-gantt-type="todo" data-gantt-project="${esc(p.id)}" data-gantt-index="${tIdx}" data-gantt-start="${formatDateISO(s)}" data-gantt-end="${formatDateISO(e)}" draggable="false"` : '';
|
||||
const handles = editable ? `<span class="gantt-resize gantt-resize-left" data-resize="left"></span><span class="gantt-resize gantt-resize-right" data-resize="right"></span>` : '';
|
||||
const isCritical = options.criticalPath && options.criticalPath.includes(todo.id);
|
||||
const progress = Math.max(0, Math.min(100, parseInt(todo.progress, 10) || 0));
|
||||
const conflictTitle = conflicts.length > 0
|
||||
? '\nAchtung: ' + conflicts.map(c => `${c.name} ${c.type.toLowerCase()} ${formatDateDE(c.start)}–${formatDateDE(c.end)}`).join('; ')
|
||||
: '';
|
||||
barHtml = `<div class="gantt-bar todo${todo.done ? ' done' : ''}${isCritical ? ' critical' : ''}${conflicts.length > 0 ? ' conflict-absence' : ''}"${dragAttrs} data-todo-id="${esc(todo.id)}" data-todo-depends="${esc((todo.dependsOn || []).join(','))}" style="left:${(offset/totalDays*100).toFixed(4)}%;width:${(span/totalDays*100).toFixed(4)}%" title="${esc(todo.title || 'TODO')}: ${formatDateDE(s)}–${formatDateDE(e)}${esc(conflictTitle)}">${handles}<div class="gantt-progress" style="width:${progress}%"></div></div>`;
|
||||
barHtml = `<div class="gantt-bar todo${todo.done ? ' done' : ''}${conflicts.length > 0 ? ' conflict-absence' : ''}"${dragAttrs} data-todo-id="${esc(todo.id)}" data-todo-depends="${esc((todo.dependsOn || []).join(','))}" style="left:${(offset/totalDays*100).toFixed(4)}%;width:${(span/totalDays*100).toFixed(4)}%" title="${esc(todo.title || 'TODO')}: ${formatDateDE(s)}–${formatDateDE(e)}${esc(conflictTitle)}">${handles}<div class="gantt-progress" style="width:${progress}%"></div></div>`;
|
||||
}
|
||||
}
|
||||
// Geister-Balken zuerst einfügen, damit der aktuelle Balken darüber liegt.
|
||||
@@ -204,10 +203,22 @@
|
||||
? `<span class="gantt-conflict-badge" title="${esc(conflicts.map(c => `${c.name}: ${c.type} ${formatDateDE(c.start)}–${formatDateDE(c.end)}`).join('\n'))}">${icon('warning')}</span>`
|
||||
: '';
|
||||
|
||||
row.innerHTML = `<div class="gantt-label" data-todo-id="${esc(todo.id)}" style="padding-left:24px;font-weight:400;color:var(--text-muted)" title="${esc(todo.title || 'TODO')}">
|
||||
// Ruhigere Namensspalte (Eriks Wahl "A"): Vorgänger stehen nicht mehr
|
||||
// als Schildchen im Namen, sondern im Tooltip (und als Linie im
|
||||
// Diagramm); rechts die Kürzel der Zuständigen.
|
||||
const vorgaengerText = depBadge ? '\nVorgänger: ' + (depBadge.match(/title="Vorgänger: ([^"]*)"/) || [,''])[1] : '';
|
||||
const vorgaengerKonflikt = depBadge.indexOf('conflict') !== -1;
|
||||
const leute = (Array.isArray(todo.assignees) ? todo.assignees : []).map(id => {
|
||||
const pers = ((app.week && app.week.people) || []).find(x => x.id === id);
|
||||
return pers ? pers.name : '';
|
||||
}).filter(Boolean);
|
||||
const leuteHtml = leute.length
|
||||
? `<span class="gantt-leute">${leute.slice(0, 3).map(k => `<span>${esc(k)}</span>`).join('')}${leute.length > 3 ? `<span>+${leute.length - 3}</span>` : ''}</span>` : '';
|
||||
row.innerHTML = `<div class="gantt-label" data-todo-id="${esc(todo.id)}" style="padding-left:24px;font-weight:400;color:var(--text-muted)" title="${esc(todo.title || 'TODO')}${vorgaengerText}">
|
||||
${editable ? '<span class="gantt-todo-drag">' + icon('grip') + '</span>' : ''}
|
||||
<span class="gantt-todo-title"${editable ? ' onclick="openTodoModal(' + tIdx + ')" style="cursor:pointer"' : ''}>${esc(todo.title || 'TODO')}${depBadge}</span>
|
||||
${conflictBadge}
|
||||
<span class="gantt-todo-title"${editable ? ' onclick="openTodoModal(' + tIdx + ')" style="cursor:pointer"' : ''}>${esc(todo.title || 'TODO')}</span>
|
||||
${vorgaengerKonflikt ? '<span class="gantt-vorgaenger-warn" title="Beginnt vor dem Ende eines Vorgängers">!</span>' : ''}
|
||||
${conflictBadge}${leuteHtml}
|
||||
</div>
|
||||
<div class="gantt-timeline">${barHtml}</div>`;
|
||||
return row;
|
||||
@@ -260,16 +271,73 @@
|
||||
}
|
||||
|
||||
if(!projectRow) return;
|
||||
// Die Projektzeile trägt die Gesamtdauer und Meilensteine ohne eigene
|
||||
// Zeile; sie heißt jetzt schlicht so, statt den Projektnamen ein
|
||||
// zweites Mal zu wiederholen.
|
||||
projectRow.classList.add('tp-projektzeile');
|
||||
const projektLabel = projectRow.querySelector('.gantt-label');
|
||||
if(projektLabel) projektLabel.textContent = 'Projekt gesamt';
|
||||
let insertAfter = projectRow;
|
||||
const insertedMilestoneIds = new Set();
|
||||
|
||||
todos.forEach((todo, tIdx) => {
|
||||
// Gliederung nach Leistungsphasen (Eriks Wahl "A"): Phasenbalken
|
||||
// (balkenPhase(), load.js) werden Gruppenköpfe; jede andere Aufgabe
|
||||
// steht unter der Phase, in deren Balken sie beginnt. Nur die
|
||||
// ANZEIGE wird umsortiert — plan.todos übernimmt die Reihenfolge erst,
|
||||
// wenn jemand eine Zeile zieht (reorderPlanRows(), plan-drag.js).
|
||||
const istPhase = t => typeof balkenPhase === 'function' && !!balkenPhase(t);
|
||||
const hatBalken = t => t.start && t.end && t.showInGantt !== false;
|
||||
const phasenKoepfe = todos.filter(t => hatBalken(t) && istPhase(t))
|
||||
.sort((a, b) => String(a.start).localeCompare(String(b.start)));
|
||||
const gruppeVon = t => {
|
||||
if(istPhase(t)) return t.id;
|
||||
const s0 = parseDateISO(t.start);
|
||||
const k = s0 && phasenKoepfe.find(p => s0 >= parseDateISO(p.start) && s0 <= parseDateISO(p.end));
|
||||
return k ? k.id : '';
|
||||
};
|
||||
let reihe = todos;
|
||||
if(phasenKoepfe.length){
|
||||
reihe = [];
|
||||
phasenKoepfe.forEach(p => { reihe.push(p); todos.forEach(t => { if(!istPhase(t) && gruppeVon(t) === p.id) reihe.push(t); }); });
|
||||
todos.forEach(t => { if(reihe.indexOf(t) === -1) reihe.push(t); });
|
||||
}
|
||||
const heuteIsoWert = formatDateISO(new Date());
|
||||
const filter = options.filter || 'alle';
|
||||
const passtFilter = t => filter === 'alle'
|
||||
|| (filter === 'meine' && options.eigenePersonId && Array.isArray(t.assignees) && t.assignees.indexOf(options.eigenePersonId) !== -1)
|
||||
|| (filter === 'ueberfaellig' && !t.done && t.end && t.end < heuteIsoWert);
|
||||
const phasenZu = options.phasenZu || {};
|
||||
let aktuelleGruppeZu = false;
|
||||
let restKopfGesetzt = false;
|
||||
const einfuegen = row => { insertAfter.parentNode.insertBefore(row, insertAfter.nextSibling); insertAfter = row; };
|
||||
|
||||
// Meilensteine ohne Aufgabenbezug: je eine eigene Zeile direkt unter
|
||||
// "Projekt gesamt", nach Datum sortiert (Eriks Meldung: ohne Aufgaben
|
||||
// standen alle Meilensteine übereinander in derselben Zeile).
|
||||
milestones.filter(m => !m.position)
|
||||
.sort((a, b) => String(a.date || '').localeCompare(String(b.date || '')))
|
||||
.forEach(m => einfuegen(buildMilestoneRow(m, milestones.indexOf(m))));
|
||||
|
||||
reihe.forEach(todo => {
|
||||
const tIdx = todos.indexOf(todo);
|
||||
if(phasenKoepfe.length && hatBalken(todo)){
|
||||
if(istPhase(todo)) aktuelleGruppeZu = !!phasenZu[balkenPhase(todo)];
|
||||
else if(!gruppeVon(todo) && !restKopfGesetzt){
|
||||
// Aufgaben außerhalb jedes Phasenbalkens bekommen einen eigenen Kopf.
|
||||
const kopf = document.createElement('div');
|
||||
kopf.className = 'gantt-row tp-gruppe';
|
||||
kopf.innerHTML = '<div class="gantt-label">Weitere Aufgaben</div><div class="gantt-timeline"></div>';
|
||||
einfuegen(kopf);
|
||||
restKopfGesetzt = true;
|
||||
aktuelleGruppeZu = false;
|
||||
}
|
||||
}
|
||||
// Meilensteine, die VOR dieser Aufgabe positioniert sind
|
||||
milestones.filter(m => m.position === 'before:' + todo.id).forEach(m => {
|
||||
const mIdx = milestones.indexOf(m);
|
||||
const row = buildMilestoneRow(m, mIdx);
|
||||
insertAfter.parentNode.insertBefore(row, insertAfter.nextSibling);
|
||||
insertAfter = row;
|
||||
if(aktuelleGruppeZu && !istPhase(todo)) row.classList.add('gantt-zu');
|
||||
einfuegen(row);
|
||||
insertedMilestoneIds.add(m.id);
|
||||
});
|
||||
|
||||
@@ -292,16 +360,40 @@
|
||||
// kippt.
|
||||
if(todo.start && todo.end && todo.showInGantt !== false){
|
||||
const todoRow = buildTodoRow(todo, tIdx);
|
||||
insertAfter.parentNode.insertBefore(todoRow, insertAfter.nextSibling);
|
||||
insertAfter = todoRow;
|
||||
if(phasenKoepfe.length && istPhase(todo)){
|
||||
const ph = balkenPhase(todo);
|
||||
const zu = !!phasenZu[ph];
|
||||
const kinder = todos.filter(t => !istPhase(t) && hatBalken(t) && gruppeVon(t) === todo.id);
|
||||
todoRow.classList.add('gantt-phase');
|
||||
const label = todoRow.querySelector('.gantt-label');
|
||||
if(label){
|
||||
const knopf = document.createElement('button');
|
||||
knopf.type = 'button';
|
||||
knopf.className = 'tp-klappen';
|
||||
knopf.setAttribute('aria-expanded', String(!zu));
|
||||
knopf.setAttribute('aria-label', (zu ? 'Aufklappen: ' : 'Zuklappen: ') + ph);
|
||||
knopf.setAttribute('onclick', `event.stopPropagation(); tpPhaseUmschalten('${ph.replace(/'/g, '')}')`);
|
||||
knopf.textContent = zu ? '▶' : '▼';
|
||||
label.insertBefore(knopf, label.firstChild);
|
||||
const info = document.createElement('span');
|
||||
info.className = 'tp-phase-info';
|
||||
info.textContent = kinder.length + ' · ' + (parseInt(todo.progress, 10) || 0) + ' %';
|
||||
info.title = (kinder.length === 1 ? '1 Aufgabe' : kinder.length + ' Aufgaben') + ', Phase zu ' + (parseInt(todo.progress, 10) || 0) + ' % erledigt';
|
||||
label.appendChild(info);
|
||||
}
|
||||
} else {
|
||||
if(aktuelleGruppeZu) todoRow.classList.add('gantt-zu');
|
||||
if(!passtFilter(todo)) todoRow.classList.add('gantt-gefiltert');
|
||||
}
|
||||
einfuegen(todoRow);
|
||||
}
|
||||
|
||||
// Meilensteine, die NACH dieser Aufgabe positioniert sind
|
||||
milestones.filter(m => m.position === 'after:' + todo.id).forEach(m => {
|
||||
const mIdx = milestones.indexOf(m);
|
||||
const row = buildMilestoneRow(m, mIdx);
|
||||
insertAfter.parentNode.insertBefore(row, insertAfter.nextSibling);
|
||||
insertAfter = row;
|
||||
if(aktuelleGruppeZu && !istPhase(todo)) row.classList.add('gantt-zu');
|
||||
einfuegen(row);
|
||||
insertedMilestoneIds.add(m.id);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user