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

+160 -6
View File
@@ -55,7 +55,7 @@
if(positionSelect){
const currentPosition = currentMs && currentMs.position ? currentMs.position : '';
const defaultAfterTodo = isNew && plan.todos && plan.todos.length > 0 ? plan.todos[plan.todos.length - 1].id : null;
let opts = '<option value="">Oben in der Projektzeile</option>';
let opts = '<option value="">Oben, eigene Zeile</option>';
if(plan.todos && plan.todos.length > 0){
plan.todos.forEach(t => {
const beforeVal = 'before:' + t.id;
@@ -69,6 +69,14 @@
}
positionSelect.innerHTML = opts;
}
window.msLageSync();
// Seitenpanel: Überschrift und "Löschen" nur beim Bearbeiten — vorher
// stand "Löschen" auch beim Anlegen eines neuen Meilensteins.
const msTitel = document.getElementById('milestoneModalTitle');
if(msTitel) msTitel.textContent = isNew ? 'Neuer Meilenstein' : 'Meilenstein bearbeiten';
const msLoeschen = document.getElementById('milestoneEditDelete');
if(msLoeschen) msLoeschen.hidden = isNew;
const dependsOnList = document.getElementById('milestoneEditDependsOn');
if(dependsOnList){
@@ -110,6 +118,35 @@
document.getElementById('milestoneEditName').focus();
};
// msLageSync/msLageSetzen: "Wo im Terminplan" als Umschalter "In der
// Projektzeile | Bei einer Aufgabe" über dem bisherigen Auswahlfeld
// (#milestoneEditPosition, Wert '' = Projektzeile, sonst before:/after:
// einer Aufgabe). Gespeichert wird weiter allein der Wert des Felds.
window.msLageSync = function(){
const sel = document.getElementById('milestoneEditPosition');
const seg = document.getElementById('milestoneEditLage');
if(!sel || !seg) return;
const beiAufgabe = !!sel.value;
const hatAufgaben = sel.options.length > 1;
seg.querySelectorAll('button').forEach(b => {
b.setAttribute('aria-pressed', String((b.dataset.lage === 'aufgabe') === beiAufgabe));
if(b.dataset.lage === 'aufgabe') b.disabled = !hatAufgaben;
});
sel.hidden = !beiAufgabe;
// Die Option "Oben, eigene Zeile" steht schon im Umschalter.
if(sel.options[0] && sel.options[0].value === '') sel.options[0].hidden = true;
};
window.msLageSetzen = function(art){
const sel = document.getElementById('milestoneEditPosition');
if(!sel) return;
if(art === 'zeile') sel.value = '';
else if(!sel.value){
const nachOptionen = Array.from(sel.options).filter(o => o.value.indexOf('after:') === 0);
if(nachOptionen.length) sel.value = nachOptionen[nachOptionen.length - 1].value;
}
window.msLageSync();
};
// closeMilestoneModal: blendet das Meilenstein-Modal aus und setzt den
// Bearbeitungsindex zurück.
window.closeMilestoneModal = function(){
@@ -186,6 +223,9 @@
// todoModalSnapshot: Stand der Eingaben beim Öffnen — Vergleichsbasis für
// die Rückfrage beim Abbrechen (siehe todoFormSnapshot()).
let todoModalSnapshot = '';
// todoNachSchliessen: Rückruf nach dem Schließen des Panels (gespeichert
// oder verworfen) — "Termin aus Mail" öffnet damit den nächsten Vorschlag.
let todoNachSchliessen = null;
// todoProgressMemo: zuletzt im Dialog eingestellter Fortschritt. "Offen" und
// "Erledigt" haben feste Werte (0/100); wer von dort zurück auf "In Arbeit"
@@ -358,6 +398,69 @@
list.querySelectorAll('.todo-depends-group').forEach(group => { group.hidden = !!query; });
};
// ---------------------------------------------------------------------------
// Leistungsphase als Balken (Eriks Wunsch): "+ Neu" → "Leistungsphase"
// öffnet dasselbe Panel im Phasen-Modus. Ein Balken ist NUR über das Feld
// t.lph eine Phase (balkenPhase(), load.js) — nicht mehr über seinen Titel.
// ---------------------------------------------------------------------------
const LPH_NAMEN = {
'LPH 1': 'Grundlagenermittlung', 'LPH 2': 'Vorplanung', 'LPH 3': 'Entwurfsplanung',
'LPH 4': 'Genehmigungsplanung', 'LPH 5': 'Ausführungsplanung', 'LPH 6': 'Vorbereitung der Vergabe',
'LPH 7': 'Mitwirkung bei der Vergabe', 'LPH 8': 'Objektüberwachung', 'LPH 9': 'Objektbetreuung', 'W+M': 'Wartung und Monitoring'
};
let todoPhasenModus = false;
let todoPhaseAutoTitel = '';
const phasenTitel = l => l ? (l + (LPH_NAMEN[l] ? ' ' + LPH_NAMEN[l] : '')) : '';
// addPlanPhase: "+ Neu" → "Leistungsphase".
// Optional lph: diese Phase vorwählen (Bereich "Leistungsphasen", Kachel
// ohne Zeitraum).
window.addPlanPhase = function(lph){ openTodoModal(null, { phase: true, lph: typeof lph === 'string' ? lph : '' }); };
// todoPhaseGewechselt: Auswahl der Phase geändert — den vorgeschlagenen
// Titel mitziehen, solange niemand ihn von Hand geändert hat.
window.todoPhaseGewechselt = function(){
const sel = document.getElementById('todoEditLph');
const titel = document.getElementById('todoEditTitle');
if(!sel || !titel) return;
if(!titel.value.trim() || titel.value === todoPhaseAutoTitel){
todoPhaseAutoTitel = phasenTitel(sel.value);
titel.value = todoPhaseAutoTitel;
}
};
// todoPhasenModusSetzen: Panel für eine Leistungsphase herrichten (Auswahl
// füllen, "Wer"/"Speichern & neu" ausblenden) oder zurück zur Aufgabe.
function todoPhasenModusSetzen(an, todo, isNew, vorgabeLph){
todoPhasenModus = an;
const abs = document.getElementById('todoEditPhaseAbs');
const wer = document.getElementById('todoEditWerAbs');
const neu = document.getElementById('todoEditSaveNew');
if(abs) abs.hidden = !an;
if(wer) wer.hidden = an;
if(neu) neu.hidden = an;
const titelEl = document.getElementById('todoModalTitle');
if(titelEl) titelEl.textContent = an ? (isNew ? 'Neue Leistungsphase' : 'Leistungsphase bearbeiten') : (isNew ? 'Neue Aufgabe' : 'Aufgabe bearbeiten');
if(!an) return;
const plan = app.ensurePlanLocal();
const vergeben = new Set((plan.todos || []).filter(t => t !== todo && t.lph).map(t => t.lph));
const beauftragt = Array.isArray(app.project.beauftragteLP) ? app.project.beauftragteLP : [];
const sel = document.getElementById('todoEditLph');
if(!sel) return;
sel.innerHTML = LPHS.map(l => {
const zusatz = vergeben.has(l) ? ' (schon im Plan)' : (beauftragt.length && !beauftragt.includes(l) ? ' (nicht beauftragt)' : '');
return `<option value="${esc(l)}"${vergeben.has(l) ? ' disabled' : ''}>${esc(phasenTitel(l) + zusatz)}</option>`;
}).join('');
const frei = l => !vergeben.has(l);
sel.value = (todo && todo.lph) || (vorgabeLph && frei(vorgabeLph) ? vorgabeLph : '') || LPHS.find(l => frei(l) && (!beauftragt.length || beauftragt.includes(l))) || LPHS.find(frei) || '';
if(isNew){
todoPhaseAutoTitel = phasenTitel(sel.value);
document.getElementById('todoEditTitle').value = todoPhaseAutoTitel;
} else {
todoPhaseAutoTitel = '';
}
}
// openTodoModal: öffnet das TODO-Modal. Parameter: index (Index der Aufgabe
// oder null für eine neue), prefill (optional, Vorbelegung für "Speichern &
// Neu": { start, end, assignees }). Befüllt Titel, Zeitraum samt sichtbarer
@@ -380,7 +483,18 @@
const titleEl = document.getElementById('todoModalTitle');
if(titleEl) titleEl.textContent = isNew ? 'Neue Aufgabe' : 'Aufgabe bearbeiten';
document.getElementById('todoEditTitle').value = isNew ? '' : (currentTodo.title || '');
document.getElementById('todoEditTitle').value = isNew ? (pre.title || '') : (currentTodo.title || '');
const notizEl = document.getElementById('todoEditNotiz');
if(notizEl) notizEl.value = isNew ? (pre.notiz || '') : (currentTodo.notiz || '');
// Hinweis über der Notiz, nur bei einem Vorschlag aus "Termin aus Mail".
const mailHinweisEl = document.getElementById('todoEditMailHinweis');
if(mailHinweisEl){
mailHinweisEl.hidden = !(isNew && pre.mailHinweis);
mailHinweisEl.textContent = (isNew && pre.mailHinweis) || '';
}
todoNachSchliessen = isNew && typeof pre.nachSchliessen === 'function' ? pre.nachSchliessen : null;
const saveNewBtn = document.getElementById('todoEditSaveNew');
if(saveNewBtn) saveNewBtn.hidden = !!todoNachSchliessen;
// Zeitraum: neben den versteckten ISO-Feldern muss auch die sichtbare
// Anzeige gesetzt werden — sonst stünde dort der Platzhalter bzw. der
@@ -404,6 +518,12 @@
// Wert (dieselben Rahmendaten wie Zeitraum/Zuständige, s. saveTodoEdit()).
const showInGanttEl = document.getElementById('todoEditShowInGantt');
if(showInGanttEl) showInGanttEl.checked = isNew ? (pre.showInGantt !== undefined ? pre.showInGantt : true) : currentTodo.showInGantt !== false;
const aufwandEl = document.getElementById('todoEditAufwand');
if(aufwandEl){
const aufwandWert = isNew ? Number(pre.aufwand) : Number(currentTodo.aufwand);
aufwandEl.value = aufwandWert > 0 ? String(aufwandWert) : '';
}
todoPhasenModusSetzen(isNew ? !!pre.phase : !!currentTodo.lph, isNew ? null : currentTodo, isNew, isNew ? pre.lph : null);
const storedProgress = isNew ? 0 : (parseInt(currentTodo.progress, 10) || 0);
todoProgressMemo = (storedProgress > 0 && storedProgress < 100) ? storedProgress : 50;
@@ -465,13 +585,25 @@
uniquePeople.push(a.person);
}
});
// Bereits Zuständige, die (nicht mehr) dem Projekt zugeordnet sind —
// etwa über "Aufgabe aus Kommentar" aus dem ganzen Teilnehmerkreis
// gewählt — ebenfalls als Chip zeigen. Ohne sie gingen sie beim
// Speichern stillschweigend verloren, weil saveTodoEdit() nur die
// gedrückten Chips liest.
currentAssignees.forEach(id => {
if(seenPeople.has(id)) return;
seenPeople.add(id);
const p = ((app.week && app.week.people) || []).find(x => x.id === id) || { id, name: id, fullName: id };
uniquePeople.push(Object.assign({}, p, { nichtImProjekt: true }));
});
// Chips statt Checkbox-Reihe: Kürzel + Name, dieselbe Lesart wie die
// Personenspalten im Projektboard.
assigneesDiv.innerHTML = uniquePeople.map(person => {
const pressed = currentAssignees.includes(person.id) ? 'true' : 'false';
const short = person.name || initialsForName(person.fullName || '') || '?';
const full = person.fullName || person.name || '';
return `<button type="button" class="person-chip" data-person-id="${esc(person.id)}" aria-pressed="${pressed}" onclick="toggleTodoAssignee(this)">`
const fremd = person.nichtImProjekt ? ' title="Nicht (mehr) dem Projekt zugeordnet"' : '';
return `<button type="button" class="person-chip${person.nichtImProjekt ? ' ist-fremd' : ''}"${fremd} data-person-id="${esc(person.id)}" aria-pressed="${pressed}" onclick="toggleTodoAssignee(this)">`
+ `<span class="person-chip-ini">${esc(short)}</span>${esc(full)}</button>`;
}).join('') || '<span class="hint">Keine Projektzuordnungen im Board.</span>';
}
@@ -512,8 +644,10 @@
const read = id => { const el = document.getElementById(id); return el ? el.value : ''; };
const deps = Array.from(document.querySelectorAll('#todoEditDependsOn input[type="checkbox"]:checked')).map(cb => cb.value);
const people = Array.from(document.querySelectorAll('#todoEditAssignees .person-chip[aria-pressed="true"]')).map(b => b.dataset.personId);
const gantt = document.getElementById('todoEditShowInGantt');
return JSON.stringify([read('todoEditTitle'), read('todoEditStart'), read('todoEditEnd'),
currentTodoEditStatus(), read('todoEditProgress'), deps, people]);
currentTodoEditStatus(), read('todoEditProgress'), deps, people,
gantt ? gantt.checked : true, read('todoEditAufwand'), read('todoEditLph'), read('todoEditNotiz')]);
}
// closeTodoModal: blendet das TODO-Modal aus und setzt den Bearbeitungsindex
@@ -527,6 +661,9 @@
document.getElementById('todoModal').style.display = 'none';
app.currentTodoIndex = null;
todoModalSnapshot = '';
const danach = todoNachSchliessen;
todoNachSchliessen = null;
if(danach) setTimeout(danach, 0);
return true;
};
@@ -573,12 +710,21 @@
const showInGanttEl = document.getElementById('todoEditShowInGantt');
const showInGantt = showInGanttEl ? showInGanttEl.checked : true;
// Aufwand in Stunden (optional): zählt zur Auslastung der Zuständigen.
const aufwandEl = document.getElementById('todoEditAufwand');
const aufwand = aufwandEl ? Math.max(0, parseFloat(String(aufwandEl.value).replace(',', '.')) || 0) : 0;
// Phasen-Modus: welche Leistungsphase dieser Balken ist.
const lphSel = document.getElementById('todoEditLph');
const lph = todoPhasenModus && lphSel ? lphSel.value : '';
const notizEl = document.getElementById('todoEditNotiz');
const notiz = notizEl ? notizEl.value.trim() : '';
if(todoPhasenModus && !lph){ alert('Bitte eine Leistungsphase wählen.'); return; }
const plan = app.ensurePlanLocal();
if(!Array.isArray(plan.todos)) plan.todos = [];
if(app.currentTodoIndex === null){
plan.todos.push({ id: uid(), title, start, end, done, progress, dependsOn, assignees, showInGantt });
plan.todos.push(Object.assign({ id: uid(), title, start, end, done, progress, dependsOn, assignees, showInGantt, aufwand, notiz }, lph ? { lph, assignees: [], aufwand: 0 } : {}));
} else if(plan.todos[app.currentTodoIndex]){
const t = plan.todos[app.currentTodoIndex];
t.title = title;
@@ -589,6 +735,9 @@
t.dependsOn = dependsOn;
t.assignees = assignees;
t.showInGantt = showInGantt;
t.aufwand = aufwand;
t.notiz = notiz;
if(todoPhasenModus) t.lph = lph;
}
app.savePlan();
// Erst schließen (setzt den Bearbeitungsindex zurück), dann ggf. direkt
@@ -633,7 +782,12 @@
if(app.isLocked()) return;
const plan = app.ensurePlanLocal();
if(!plan.todos[idx]) return;
plan.todos[idx].done = !plan.todos[idx].done;
const t = plan.todos[idx];
t.done = !t.done;
// Fortschritt mitführen: Sonst blieb nach dem Zurücknehmen "100 %" stehen,
// und der Dialog las daraus wieder "fertig" (todoStatusFrom()).
if(t.done) t.progress = 100;
else if(Number(t.progress) >= 100) t.progress = 0;
app.savePlan();
renderPlanGantt();
};