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
@@ -4,7 +4,8 @@
|
||||
// In einem Ingenieurbüro wiederholt sich der Ablauf einer Leistungsphase über
|
||||
// alle Projekte hinweg. Dieses Modul macht daraus wiederverwendbare Vorlagen:
|
||||
// Der Button "+ Vorlage" in der GANTT-Toolbar öffnet #lphTemplateModal, dort
|
||||
// wählt man eine Leistungsphase und ein Startdatum, hakt die gewünschten
|
||||
// wählt man Gewerk, Leistungsphase und Startdatum (Vorlagen gibt es je
|
||||
// Gewerk), hakt die gewünschten
|
||||
// Schritte an und fügt sie mit einem Klick als TODOs in den Projektplan ein.
|
||||
//
|
||||
// Der Honorar-Terminplan (js/plan/plan-honorar.js) ist ein eigener Dialog mit
|
||||
@@ -16,7 +17,7 @@
|
||||
// lph_templates): GET für alle angemeldeten Nutzer, POST/DELETE nur für
|
||||
// Admins — das Einfügen darf jeder, das Pflegen der firmenweiten Vorlagen
|
||||
// nicht. Die Pflege-Maske selbst steht seit dem Umbau NICHT mehr in diesem
|
||||
// Dialog, sondern im Bereich „Einstellungen" (js/settings/settings.js) —
|
||||
// Dialog, sondern im Admin-Bereich „Aufgaben-Vorlagen" (js/admin/admin.js) —
|
||||
// hier bleibt für Administratoren nur ein Verweis dorthin.
|
||||
//
|
||||
// WICHTIG: Eingefügte Aufgaben sind danach ganz normale plan_todos ohne
|
||||
@@ -46,13 +47,28 @@
|
||||
if(app.isLocked()) return;
|
||||
|
||||
const lphSelect = document.getElementById('templateLphSelect');
|
||||
if(lphSelect && !lphSelect.options.length){
|
||||
// Bei JEDEM Öffnen neu vorbelegen: Der Projektbereich zeigt nacheinander
|
||||
// verschiedene Projekte im selben Dokument — sonst blieb die amtierende
|
||||
// Leistungsphase des zuerst geöffneten Projekts vorgewählt.
|
||||
if(lphSelect){
|
||||
// Vorbelegung: die amtierende Leistungsphase des Projekts, sonst LPH 1
|
||||
const aktLp = Array.isArray(app.project.aktuelleLP) ? app.project.aktuelleLP[0] : '';
|
||||
lphSelect.innerHTML = TEMPLATE_LPHS.map(l =>
|
||||
`<option value="${esc(l)}" ${l === aktLp ? 'selected' : ''}>${esc(l)}</option>`
|
||||
).join('');
|
||||
}
|
||||
// Gewerk vorbelegen: das eigene, wenn das Projekt es hat, sonst das
|
||||
// erste Gewerk des Projekts, sonst das eigene (Vorlagen gibt es je
|
||||
// Gewerk, Eriks Wunsch).
|
||||
const gewSelect = document.getElementById('templateGewerkSelect');
|
||||
if(gewSelect){
|
||||
const projGew = Array.isArray(app.project.gewerke) ? app.project.gewerke : [];
|
||||
const eigen = currentUser && Object.keys(DEPARTMENTS).includes(currentUser.department) ? currentUser.department : '';
|
||||
const vorwahl = (eigen && projGew.includes(eigen)) ? eigen : (projGew[0] || eigen || Object.keys(DEPARTMENTS)[0]);
|
||||
gewSelect.innerHTML = Object.keys(DEPARTMENTS).map(d =>
|
||||
`<option value="${esc(d)}" ${d === vorwahl ? 'selected' : ''}>${esc(d)}${projGew.includes(d) ? '' : ' (nicht im Projekt)'}</option>`
|
||||
).join('');
|
||||
}
|
||||
// Startdatum vorbelegen: heute
|
||||
const startInput = document.getElementById('templateStart');
|
||||
const startDisplay = document.getElementById('templateStartDisplay');
|
||||
@@ -62,6 +78,7 @@
|
||||
if(startDisplay) startDisplay.value = formatDateDE(parseDateISO(today));
|
||||
}
|
||||
|
||||
templatePhaseVorbelegen();
|
||||
document.getElementById('lphTemplateModal').style.display = 'flex';
|
||||
loadTemplates();
|
||||
};
|
||||
@@ -88,12 +105,88 @@
|
||||
});
|
||||
}
|
||||
|
||||
// templatePhaseVorbelegen: Hat das Projekt einen Phasenbalken der gewählten
|
||||
// Leistungsphase, übernehmen Startdatum und „Dauer der Phase“ seinen
|
||||
// Zeitraum (in Arbeitstagen). Sonst bleibt das Startdatum, die Dauer ist
|
||||
// von Hand anzugeben. Keine Parameter.
|
||||
window.templatePhaseVorbelegen = function(){
|
||||
const lph = (document.getElementById('templateLphSelect') || {}).value || '';
|
||||
const todos = (app.ensurePlanLocal().todos || []);
|
||||
const balken = todos.find(t => typeof balkenPhase === 'function' && balkenPhase(t) === lph && t.start && t.end);
|
||||
const tageEl = document.getElementById('templatePhasenTage');
|
||||
const hinweis = document.getElementById('templatePhasenHinweis');
|
||||
if(balken){
|
||||
const s = parseDateISO(balken.start), e = parseDateISO(balken.end);
|
||||
const startInput = document.getElementById('templateStart');
|
||||
const startDisplay = document.getElementById('templateStartDisplay');
|
||||
if(startInput) startInput.value = balken.start;
|
||||
if(startDisplay) startDisplay.value = formatDateDE(s);
|
||||
if(tageEl) tageEl.value = Math.max(1, workdaysBetween(s, e));
|
||||
if(hinweis) hinweis.textContent = 'aus dem Phasenbalken ' + lph + ' (' + formatDateDE(s) + ' – ' + formatDateDE(e) + ')';
|
||||
} else {
|
||||
if(tageEl) tageEl.value = '';
|
||||
if(hinweis) hinweis.textContent = 'Kein Phasenbalken für ' + lph + ' — Dauer bitte angeben.';
|
||||
}
|
||||
};
|
||||
|
||||
// templatePlan: Termine der Schritte nach ihrem Anteil (Eriks Wunsch, z. B.
|
||||
// Siemon-Tabelle): Jeder Schritt mit Anteil bekommt Anteil ÷ Summe aller
|
||||
// Anteile der Phase an deren Dauer, nacheinander ab dem Startdatum in der
|
||||
// gepflegten Reihenfolge (Arbeitstage, ohne Wochenenden/Feiertage). Die
|
||||
// Grenzen werden über die laufende Summe gerundet, damit sich keine
|
||||
// Rundungsfehler aufaddieren. Schritte ohne Anteil: Start/Dauer der
|
||||
// Vorlage, ohne Dauer ohne Termin. Parameter: entries (alle Schritte der
|
||||
// Phase), startDate, phasenTage. Rückgabe: Map id → {s, e} oder null.
|
||||
function templatePlan(entries, startDate, phasenTage){
|
||||
const out = new Map();
|
||||
const summe = entries.reduce((a, t) => a + (Number(t.anteil) || 0), 0);
|
||||
let kum = 0;
|
||||
entries.forEach(t => {
|
||||
const ant = Number(t.anteil) || 0;
|
||||
if(ant > 0 && summe > 0){
|
||||
if(!startDate || !(phasenTage > 0)){ out.set(t.id, null); return; }
|
||||
const von = Math.round(phasenTage * kum / summe);
|
||||
const bis = Math.round(phasenTage * (kum + ant) / summe);
|
||||
kum += ant;
|
||||
const s = addWorkdays(startDate, von);
|
||||
const e = addWorkdays(s, Math.max(1, bis - von) - 1);
|
||||
out.set(t.id, { s, e, anteil: ant / summe });
|
||||
} else if(t.enthaltenIn){
|
||||
out.set(t.id, null); // zweiter Durchgang unten
|
||||
} else if(t.durationDays > 0 && startDate){
|
||||
const s = addDays(startDate, t.offsetDays);
|
||||
out.set(t.id, { s, e: addDays(s, t.durationDays - 1) });
|
||||
} else {
|
||||
out.set(t.id, null);
|
||||
}
|
||||
});
|
||||
// „in a) enthalten“ (Siemon): zeitgleich mit den genannten Schritten —
|
||||
// vom frühesten Beginn bis zum spätesten Ende. Der Buchstabe eines
|
||||
// Schritts steht am Ende seines Titels („… (a)“).
|
||||
const buchstabe = t => (String(t.title).match(/\(([a-z])\)\s*$/i) || [])[1];
|
||||
entries.filter(t => t.enthaltenIn).forEach(t => {
|
||||
const wirte = t.enthaltenIn.split(',')
|
||||
.map(b => entries.find(x => !x.enthaltenIn && (buchstabe(x) || '').toLowerCase() === b))
|
||||
.map(x => x && out.get(x.id)).filter(Boolean);
|
||||
if(!wirte.length) return;
|
||||
out.set(t.id, {
|
||||
s: new Date(Math.min(...wirte.map(w => w.s.getTime()))),
|
||||
e: new Date(Math.max(...wirte.map(w => w.e.getTime()))),
|
||||
enthaltenIn: t.enthaltenIn
|
||||
});
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
// templatesForSelectedLph: die Vorlagen-Schritte der gerade gewählten
|
||||
// Leistungsphase, in ihrer gepflegten Reihenfolge. Rückgabe: Array.
|
||||
function templatesForSelectedLph(){
|
||||
const lphSelect = document.getElementById('templateLphSelect');
|
||||
const gewSelect = document.getElementById('templateGewerkSelect');
|
||||
const lph = lphSelect ? lphSelect.value : '';
|
||||
return templateCache.filter(t => t.lph === lph);
|
||||
const g = gewSelect ? gewSelect.value : '';
|
||||
// Vorlagen ohne Gewerk (Altbestand) gelten für alle Gewerke
|
||||
return templateCache.filter(t => t.lph === lph && (!t.gewerk || t.gewerk === g));
|
||||
}
|
||||
|
||||
// renderTemplateList: baut die Auswahlliste der Schritte (Checkbox je
|
||||
@@ -107,18 +200,23 @@
|
||||
const entries = templatesForSelectedLph();
|
||||
const startIso = (document.getElementById('templateStart') || {}).value || '';
|
||||
const startDate = parseDateISO(startIso);
|
||||
const phasenTage = parseInt((document.getElementById('templatePhasenTage') || {}).value, 10) || 0;
|
||||
const plan = templatePlan(entries, startDate, phasenTage);
|
||||
|
||||
if(entries.length === 0){
|
||||
listEl.innerHTML = '<span class="hint">Für diese Leistungsphase ist noch keine Vorlage hinterlegt.</span>';
|
||||
listEl.innerHTML = '<span class="hint">Für dieses Gewerk und diese Leistungsphase ist noch keine Vorlage hinterlegt.</span>';
|
||||
} else {
|
||||
listEl.innerHTML = entries.map(t => {
|
||||
// Vorschau der konkreten Termine (nur wenn ein Startdatum gesetzt ist)
|
||||
let dateHint = `Tag +${t.offsetDays}, ${t.durationDays} Tag${t.durationDays === 1 ? '' : 'e'}`;
|
||||
if(startDate){
|
||||
const s = addDays(startDate, t.offsetDays);
|
||||
const e = addDays(s, Math.max(0, t.durationDays - 1));
|
||||
dateHint = formatDateDE(s) + '–' + formatDateDE(e);
|
||||
}
|
||||
const termin = plan.get(t.id);
|
||||
let dateHint;
|
||||
if(termin) dateHint = formatDateDE(termin.s) + '–' + formatDateDE(termin.e)
|
||||
+ (termin.anteil ? ' · ' + String(t.anteil).replace('.', ',') + ' %' : '')
|
||||
+ (termin.enthaltenIn ? ' · in ' + termin.enthaltenIn.split(',').map(x => x + ')').join(' + ') : '');
|
||||
else if(t.enthaltenIn) dateHint = 'in ' + t.enthaltenIn.split(',').map(x => x + ')').join(' + ') + ' — Dauer der Phase fehlt';
|
||||
else if(Number(t.anteil) > 0) dateHint = String(t.anteil).replace('.', ',') + ' % — Dauer der Phase fehlt';
|
||||
else if(t.durationDays > 0) dateHint = `Tag +${t.offsetDays}, ${t.durationDays} Tag${t.durationDays === 1 ? '' : 'e'}`;
|
||||
else dateHint = 'ohne Termin';
|
||||
return `<label class="todo-depends-item">
|
||||
<input type="checkbox" value="${esc(t.id)}" checked>
|
||||
<span style="flex:1">${esc(t.title)}</span>
|
||||
@@ -131,13 +229,13 @@
|
||||
};
|
||||
|
||||
// renderTemplateHint: blendet für Administratoren den Verweis auf den
|
||||
// Bereich „Einstellungen" ein. Das Anlegen und Löschen von Schritten stand
|
||||
// bis zum Umbau hier im Dialog; es liegt jetzt in js/settings/settings.js.
|
||||
// Admin-Bereich „Aufgaben-Vorlagen" ein. Das Anlegen und Löschen von Schritten stand
|
||||
// bis zum Umbau hier im Dialog; es liegt jetzt in js/admin/admin.js.
|
||||
// Dieser Dialog FÜGT Vorlagen nur noch ein. Keine Parameter, keine Rückgabe.
|
||||
function renderTemplateHint(){
|
||||
const el = document.getElementById('templateAdminHint');
|
||||
if(!el) return;
|
||||
el.style.display = (currentUser && currentUser.role === 'admin') ? '' : 'none';
|
||||
el.style.display = hatRecht('a.vorlagen') ? '' : 'none';
|
||||
}
|
||||
|
||||
// applyTemplate: erzeugt aus den angehakten Vorlagen-Schritten neue TODOs im
|
||||
@@ -149,7 +247,6 @@
|
||||
if(app.isLocked()) return;
|
||||
const startIso = (document.getElementById('templateStart') || {}).value || '';
|
||||
const startDate = parseDateISO(startIso);
|
||||
if(!startDate){ alert('Bitte ein Startdatum wählen.'); return; }
|
||||
|
||||
const listEl = document.getElementById('templateTaskList');
|
||||
const checkedIds = listEl
|
||||
@@ -157,16 +254,26 @@
|
||||
: [];
|
||||
if(checkedIds.length === 0){ alert('Bitte mindestens einen Schritt auswählen.'); return; }
|
||||
|
||||
const entries = templatesForSelectedLph().filter(t => checkedIds.includes(t.id));
|
||||
const alle = templatesForSelectedLph();
|
||||
const entries = alle.filter(t => checkedIds.includes(t.id));
|
||||
// Ein Startdatum braucht es nur für Schritte mit Anteil oder Dauer —
|
||||
// ohne beides „ohne Termin“ (Aufgabe nur in der Liste, nicht im Gantt).
|
||||
if(!startDate && entries.some(t => t.durationDays > 0 || Number(t.anteil) > 0 || t.enthaltenIn)){ alert('Bitte ein Startdatum wählen.'); return; }
|
||||
const phasenTage = parseInt((document.getElementById('templatePhasenTage') || {}).value, 10) || 0;
|
||||
if(!phasenTage && entries.some(t => Number(t.anteil) > 0 || t.enthaltenIn)){ alert('Bitte die Dauer der Phase in Arbeitstagen angeben — die Schritte bekommen ihren Anteil davon.'); return; }
|
||||
// Anteile beziehen sich auf ALLE Schritte der Phase: Abgewählte lassen
|
||||
// ihre Lücke, die übrigen behalten ihren Platz im Ablauf.
|
||||
const termine = templatePlan(alle, startDate, phasenTage);
|
||||
const plan = app.ensurePlanLocal();
|
||||
entries.forEach(t => {
|
||||
const s = addDays(startDate, t.offsetDays);
|
||||
const e = addDays(s, Math.max(0, t.durationDays - 1));
|
||||
const termin = termine.get(t.id);
|
||||
const s = termin ? termin.s : null;
|
||||
const e = termin ? termin.e : null;
|
||||
plan.todos.push({
|
||||
id: uid(),
|
||||
title: t.title,
|
||||
start: formatDateISO(s),
|
||||
end: formatDateISO(e),
|
||||
start: s ? formatDateISO(s) : '',
|
||||
end: e ? formatDateISO(e) : '',
|
||||
done: false,
|
||||
progress: 0,
|
||||
dependsOn: [],
|
||||
|
||||
Reference in new issue
Block a user