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:
erikandClaude Opus 5.5 committed 2026-10-29 14:16:52 +01:00
1 parent f4fb3c550b
commit b7e0bd0d92
56 files changed
+12265 -2974

No files matched your search

+109 -5
View File
@@ -150,6 +150,9 @@ function buildGanttHtml(projects, start, end, options = {}){
// Kopfzeile: Monate und Tage
let headerMonths = '<div class="gantt-row gantt-header">';
// Den Griff zum Ändern der Spaltenbreite setzt ganttGriffeEinsetzen() nach
// dem Rendern in JEDE Zeile (auch die nachträglich eingefügten Aufgaben-
// und Meilensteinzeilen aus js/plan/plan-build-gantt.js).
headerMonths += '<div class="gantt-label">Projekt</div>';
let monthsHtml = '';
let current = new Date(start.getTime());
@@ -280,10 +283,11 @@ function buildGanttHtml(projects, start, end, options = {}){
}
}
// Meilensteine: nur die nicht positionierten (keine position) in der Projektzeile anzeigen.
// Positionierte Meilensteine werden vom Aufrufer (z.B. Projektplan) als separate Zeilen eingefügt.
// Meilensteine in der Projektzeile nur ohne showTodos. Im Projektplan
// bekommt jeder Meilenstein eine eigene Zeile (plan-build-gantt.js) —
// sonst lagen mehrere ohne Aufgabenbezug übereinander in einer Zeile.
milestones.forEach((m, mIdx) => {
if(options.showTodos && m.position) return;
if(options.showTodos) return;
const d = parseDateISO(m.date);
if(!d || d < start || d > end) return;
const offset = daysBetween(start, d);
@@ -298,7 +302,7 @@ function buildGanttHtml(projects, start, end, options = {}){
// 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"`;
chartStyle = ` style="min-width:${(ganttSpaltenBreite() + Math.ceil(totalDays * pxPerDay)).toFixed(0)}px"`;
}
return `<div class="gantt-chart"${chartStyle}>${body}</div>`;
}
@@ -345,4 +349,104 @@ function springZuEinstellungen(pid){
function closePlanPage(){
const back = (typeof previousTab === 'string' && previousTab && previousTab !== 'plan') ? previousTab : 'board';
showTab(back);
}
}
// ---------- Breite der Spalte "Projekt" (Eriks Wunsch: "wie in Excel") ----------
//
// Die Breite steht in der CSS-Variable --gantt-label-w (styles.css,
// .gantt-label), damit alle Zeilen beim Ziehen gleichzeitig mitgehen, ohne
// das Diagramm neu aufzubauen. Gemerkt wird sie je Browser in localStorage —
// reiner Darstellungszustand wie die eingeklappten Dashboard-Projekte.
const GANTT_SPALTE_KEY = 'projuebersicht-gantt-spalte';
const GANTT_SPALTE_MIN = 120, GANTT_SPALTE_MAX = 640, GANTT_SPALTE_STANDARD = 220;
// ganttSpaltenBreite: aktuelle Breite in Pixeln (gespeichert oder Standard).
function ganttSpaltenBreite(){
let w = GANTT_SPALTE_STANDARD;
try { w = Number(localStorage.getItem(GANTT_SPALTE_KEY)) || GANTT_SPALTE_STANDARD; } catch(e){ /* Standard */ }
return Math.max(GANTT_SPALTE_MIN, Math.min(GANTT_SPALTE_MAX, w));
}
// ganttSpalteSetzen: Breite anwenden; speichern=true merkt sie dauerhaft.
function ganttSpalteSetzen(w, speichern){
w = Math.round(Math.max(GANTT_SPALTE_MIN, Math.min(GANTT_SPALTE_MAX, w)));
document.documentElement.style.setProperty('--gantt-label-w', w + 'px');
if(speichern){ try { localStorage.setItem(GANTT_SPALTE_KEY, String(w)); } catch(e){ /* nur Komfort */ } }
}
ganttSpalteSetzen(ganttSpaltenBreite(), false);
// ganttGriffeEinsetzen: hängt an den rechten Rand jeder Namenszelle einen
// Griff — die Spaltengrenze lässt sich so in jeder Zeile fassen, nicht nur in
// der Kopfzeile (Eriks Wunsch). click/dblclick werden abgefangen, damit ein
// Klick auf die Grenze nicht zusätzlich die Aufgabe der Zeile öffnet.
// Parameter: root (Element mit dem Diagramm). Keine Rückgabe.
function ganttGriffeEinsetzen(root){
if(!root) return;
root.querySelectorAll('.gantt-chart .gantt-label').forEach(label => {
if(label.querySelector(':scope > .gantt-spalten-griff')) return;
const g = document.createElement('span');
g.className = 'gantt-spalten-griff';
g.setAttribute('role', 'separator');
g.setAttribute('aria-orientation', 'vertical');
g.title = 'Spaltenbreite ziehen, Doppelklick passt sie an';
g.addEventListener('pointerdown', ganttSpalteZiehen);
g.addEventListener('mousedown', e => e.stopPropagation());
g.addEventListener('click', e => { e.stopPropagation(); e.preventDefault(); });
g.addEventListener('dblclick', e => { e.stopPropagation(); e.preventDefault(); ganttSpalteAnpassen(); });
label.appendChild(g);
});
}
// ganttPfeileNachziehen: zeichnet die Abhängigkeitspfeile neu, höchstens
// einmal je Bild. Die Pfeile sind ein SVG mit festen Pixelkoordinaten; ohne
// das blieben sie beim Ziehen der Spalte an der alten Stelle stehen, während
// die Balken mitwandern.
let ganttPfeileGeplant = false;
function ganttPfeileNachziehen(){
if(ganttPfeileGeplant || typeof renderGanttDependencies !== 'function') return;
ganttPfeileGeplant = true;
requestAnimationFrame(() => { ganttPfeileGeplant = false; renderGanttDependencies(); });
}
// ganttNeuZeichnen: nach dem Ändern einmal neu aufbauen — die Mindestbreite
// des Diagramms bei hohem Zoom hängt von der Spaltenbreite ab.
function ganttNeuZeichnen(){
if(typeof renderPlanGantt === 'function' && typeof currentTab !== 'undefined' && currentTab === 'plan') renderPlanGantt();
}
// ganttSpalteZiehen: Ziehen am Griff (Maus, Stift, Touch). Parameter: e
// (pointerdown-Ereignis). Keine Rückgabe.
function ganttSpalteZiehen(e){
if(e.button !== undefined && e.button !== 0) return;
e.preventDefault(); e.stopPropagation();
const griff = e.currentTarget;
const startX = e.clientX, startW = ganttSpaltenBreite();
griff.setPointerCapture && griff.setPointerCapture(e.pointerId);
document.body.classList.add('gantt-spalte-zieht');
let w = startW;
const bewegen = ev => { w = startW + (ev.clientX - startX); ganttSpalteSetzen(w, false); ganttPfeileNachziehen(); };
const ende = () => {
griff.removeEventListener('pointermove', bewegen);
griff.removeEventListener('pointerup', ende);
griff.removeEventListener('pointercancel', ende);
document.body.classList.remove('gantt-spalte-zieht');
ganttSpalteSetzen(w, true);
ganttNeuZeichnen();
};
griff.addEventListener('pointermove', bewegen);
griff.addEventListener('pointerup', ende);
griff.addEventListener('pointercancel', ende);
}
// ganttSpalteAnpassen: Doppelklick auf den Griff — Breite an den längsten
// Eintrag anpassen (wie der Doppelklick auf die Spaltengrenze in Excel).
function ganttSpalteAnpassen(){
let max = 0;
document.querySelectorAll('.gantt-chart .gantt-label').forEach(el => {
// scrollWidth = volle Inhaltsbreite samt Innenabstand, auch wenn der
// Text gerade mit "…" abgeschnitten ist.
max = Math.max(max, el.scrollWidth);
});
if(!max) return;
ganttSpalteSetzen(max + 6, true);
ganttNeuZeichnen();
}