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

+39 -91
View File
@@ -9,7 +9,7 @@
// Die Schritte:
//
// 1. PROJEKT Nummer, Bezeichnung, GEWERKE, Gruppe, Schlagwörter
// 2. AUFTRAG Auftragsstatus, Honorar ODER Projektgröße, beauftragte
// 2. AUFTRAG Auftragsstatus, Honorar (freiwillig), beauftragte
// Leistungsphasen
// 3. STAND amtierende Leistungsphase
// 4. TEAM Beteiligte je Rolle — eingeschränkt auf die Gewerke des
@@ -20,10 +20,10 @@
//
// Warum ein Assistent: Honorar, Team und Schlagwörter mussten zuvor NACH dem
// Anlegen an drei weiteren Stellen nachgetragen werden — und wurden es meist
// nicht. Genau daher rühren die Projekte, die in der Auslastung als "nicht
// bewertbar" erscheinen.
// nicht.
//
// JEDER SCHRITT IST PFLICHT: "Weiter" bleibt gesperrt, solange etwas fehlt,
// Pflichtangaben sperren "Weiter", solange etwas fehlt (das Honorar ist
// freiwillig, Eriks Vorgabe 10/2026),
// und unter dem Knopf steht, was fehlt. Ein Assistent, der unvollständige
// Projekte durchlässt, löst das Problem nicht, das er lösen soll.
//
@@ -64,7 +64,7 @@ function openNewProjWizard(){
schritt: 0,
fertig: null, // angelegtes Projekt, solange der Abschluss steht
nr: '', name: '', gewerke: [], gruppe: 'Aktive Projekte', tags: [],
status: AUFTRAGSSTATUS_VORGABE, grundlage: 'honorar', honorar: '', groesse: '',
status: AUFTRAGSSTATUS_VORGABE, honorar: '',
beauftragt: [], amtierend: '',
team: {}, fremde: [],
planErzeugen: true, planStart: heuteIso(), planPersonen: '',
@@ -133,24 +133,12 @@ function wizardHonorar(){
const n = parseFloat(String(wizard.honorar).replace(',', '.'));
return (!Number.isFinite(n) || n <= 0) ? 0 : n;
}
function wizardGroesse(){
const n = parseFloat(String(wizard.groesse).replace(',', '.'));
if(!Number.isFinite(n) || n <= 0) return 0;
return Math.min(1, n);
}
// wizardBasis: der Geldbetrag, mit dem gerechnet wird. Bei der Projektgröße
// über das firmenweite Referenzhonorar — der Betrag wird bewusst NICHT
// angezeigt: Die Größe ist eine Schätzung, und eine daneben stehende
// Eurosumme gäbe ihr eine Genauigkeit, die sie nicht hat.
// wizardBasis: der Geldbetrag für den Terminplan — allein das Honorar.
// Es ist beim Anlegen freiwillig (Eriks Vorgabe); ohne Honorar entsteht
// kein Terminplan, er lässt sich später im Projektbereich erzeugen.
function wizardBasis(){
if(wizard.grundlage === 'honorar'){
const h = wizardHonorar();
return h > 0 ? { betrag: h, quelle: 'Honorar' } : { betrag: 0, quelle: null };
}
const g = wizardGroesse();
const referenz = Number(APP_SETTINGS.referenzHonorar) || 250000;
return g > 0 ? { betrag: g * referenz, quelle: 'Projektgröße' } : { betrag: 0, quelle: null };
const h = wizardHonorar();
return h > 0 ? { betrag: h, quelle: 'Honorar' } : { betrag: 0, quelle: null };
}
// wizardPlanPersonen: wie viele gleichzeitig an einer Phase arbeiten.
@@ -168,7 +156,7 @@ function wizardPlanPersonen(){
// aus dem Honorar gerechnet werden kann).
function wizardPlanZeilen(){
const b = wizardBasis();
if(wizard.grundlage !== 'honorar' || b.betrag <= 0) return [];
if(b.betrag <= 0) return [];
const phasen = wizard.beauftragt.length ? wizard.beauftragt : AUFWAND_LPS;
const summe = phasen.reduce((s, l) => s + (Number(HONORAR_ANTEILE[l]) || 0), 0);
if(summe <= 0) return [];
@@ -202,9 +190,8 @@ function wizardPlanZeilen(){
// Klartexten; leer = der Schritt ist vollständig.
//
// Der Assistent lässt niemanden weiter, solange hier etwas steht: Genau die
// Lücken, die sonst entstehen (kein Honorar, keine Phase, niemand
// zugeordnet), sind der Grund, warum Projekte später in der Auslastung nicht
// bewertbar sind.
// Lücken, die sonst entstehen (keine Phase, niemand zugeordnet), fielen
// später im Wochenbericht und im Projektbereich auf.
function wizardFehlt(key){
const fehlt = [];
if(key === 'projekt'){
@@ -213,8 +200,6 @@ function wizardFehlt(key){
if(!wizard.gewerke.length) fehlt.push('mindestens ein Gewerk');
}
if(key === 'auftrag'){
if(wizard.grundlage === 'honorar' && wizardHonorar() <= 0) fehlt.push('Honorar');
if(wizard.grundlage === 'groesse' && wizardGroesse() <= 0) fehlt.push('Projektgröße');
if(!wizard.beauftragt.length) fehlt.push('mindestens eine beauftragte Leistungsphase');
}
if(key === 'stand'){
@@ -224,7 +209,7 @@ function wizardFehlt(key){
if(wizardTeamAnzahl() === 0) fehlt.push('mindestens eine beteiligte Person');
}
if(key === 'terminplan'){
if(wizard.grundlage === 'honorar' && wizard.planErzeugen && !wizard.planStart) fehlt.push('Beginn');
if(wizardHonorar() > 0 && wizard.planErzeugen && !wizard.planStart) fehlt.push('Beginn');
}
return fehlt;
}
@@ -317,35 +302,19 @@ function wizardProjektHtml(){
}
function wizardAuftragHtml(){
const perHonorar = wizard.grundlage === 'honorar';
return `
<div class="form-row">
<label>Auftragsstatus</label>
<select onchange="wizardSetz('status', this.value)">
${auftragsstatusOptionsHtml(wizard.status)}
</select>
<span class="hint">Anfrage und Angebot wandern in die Gruppe „In Anbahnung" und zählen nicht zur Auslastung.</span>
<span class="hint">Anfrage und Angebot wandern in die Gruppe „In Anbahnung".</span>
</div>
<div class="form-row">
<label>Grundlage der Rechnung</label>
<div class="wiz-chips">
<button type="button" class="wiz-chip" aria-pressed="${perHonorar}" onclick="wizardSetz('grundlage','honorar')">Vereinbartes Honorar</button>
<button type="button" class="wiz-chip" aria-pressed="${!perHonorar}" onclick="wizardSetz('grundlage','groesse')">Geschätzte Projektgröße</button>
</div>
</div>
<div class="wiz-zeile">
<div class="form-row">
<label>Honorar (netto, €) ${perHonorar ? '<span class="wiz-pflicht">*</span>' : ''}</label>
<input type="number" id="wizHonorar" min="0" step="1000" value="${esc(wizard.honorar)}" placeholder="z. B. 480000"
${perHonorar ? `oninput="wizardSetz('honorar', this.value)"` : 'disabled'}>
<span class="hint" id="wizHinweisHonorar">${wizardHinweisHonorar()}</span>
</div>
<div class="form-row">
<label>Projektgröße (0,00 – 1,00) ${!perHonorar ? '<span class="wiz-pflicht">*</span>' : ''}</label>
<input type="number" id="wizGroesse" min="0" max="1" step="0.05" value="${esc(wizard.groesse)}" placeholder="z. B. 0,40"
${!perHonorar ? `oninput="wizardSetz('groesse', this.value)"` : 'disabled'}>
<span class="hint" id="wizHinweisGroesse">${wizardHinweisGroesse()}</span>
</div>
<label>Honorar (netto, €)</label>
<input type="number" id="wizHonorar" min="0" step="1000" value="${esc(wizard.honorar)}" placeholder="z. B. 480000"
oninput="wizardSetz('honorar', this.value)">
<span class="hint" id="wizHinweisHonorar">${wizardHinweisHonorar()}</span>
</div>
<div class="form-row">
<label>Beauftragte Leistungsphasen <span class="wiz-pflicht">*</span></label>
@@ -358,18 +327,13 @@ function wizardAuftragHtml(){
</div>`;
}
// Die beiden Hinweistexte stehen als eigene Funktionen da, weil sie beim
// Tippen mitlaufen müssen, ohne dass das Eingabefeld neu gebaut wird — sonst
// springt der Cursor ans Feldende.
// Der Hinweistext steht als eigene Funktion da, weil er beim Tippen
// mitläuft, ohne dass das Eingabefeld neu gebaut wird — sonst springt der
// Cursor ans Feldende.
function wizardHinweisHonorar(){
return wizard.grundlage === 'honorar'
? 'Grundlage für Leistungsstand, Auslastung und Terminplan.'
: 'Gesperrt — es wird mit der Projektgröße gerechnet.';
}
function wizardHinweisGroesse(){
return wizard.grundlage === 'groesse'
? 'Grober Umfang des Projekts. 0 = keine Angabe, 1 = großes Projekt.'
: 'Gesperrt — es wird mit dem Honorar gerechnet.';
return wizardHonorar() > 0
? 'Grundlage für Leistungsstand und Terminplan.'
: 'Freiwillig. Ohne Honorar entsteht kein Terminplan; es lässt sich später in den Projekteinstellungen nachtragen.';
}
function wizardStandHtml(){
@@ -382,8 +346,7 @@ function wizardStandHtml(){
<button type="button" class="wiz-chip" aria-pressed="${wizard.amtierend === l}"
onclick="wizardSetz('amtierend','${esc(l)}')">${esc(l)}</button>`).join('')}
</div>
<span class="hint">Woran gerade gearbeitet wird. Ohne diese Angabe erschiene das Projekt
in der Auslastung als nicht bewertbar.</span>
<span class="hint">Woran gerade gearbeitet wird.</span>
</div>`;
}
@@ -443,14 +406,11 @@ function wizardTeamHtml(){
}
function wizardTerminplanHtml(){
// Aus der Projektgröße lässt sich kein Terminplan rechnen: Sie ist eine
// Schätzung des Umfangs, kein vereinbarter Betrag. Termine, die daraus
// entstünden, sähen im Gantt so verbindlich aus wie errechnete.
if(wizard.grundlage !== 'honorar'){
// Ohne Honorar kein Terminplan — er lässt sich später im Projektbereich
// erzeugen („Terminplan aus Honorar").
if(wizardHonorar() <= 0){
return `<div class="wiz-sperre">
<b>Kein Terminplan aus der Projektgröße.</b>
<p>Die Größe ist eine Schätzung des Umfangs, kein vereinbarter Betrag. Termine, die daraus
entstünden, sähen im Terminplan so verbindlich aus wie errechnete — ohne es zu sein.</p>
<b>Kein Terminplan ohne Honorar.</b>
<p>Sobald das Honorar feststeht, lässt sich der Terminplan im Projektbereich jederzeit
nachträglich erzeugen („Terminplan aus Honorar").</p>
</div>`;
@@ -537,9 +497,7 @@ function wizardPruefenHtml(){
${zeile('Gruppe', esc(wizard.gruppe))}
${zeile('Schlagwörter', wizard.tags.length ? esc(wizard.tags.join(', ')) : 'keine', !wizard.tags.length)}
${zeile('Auftragsstatus', esc(auftragsstatusLabel(wizard.status)))}
${zeile('Grundlage', wizard.grundlage === 'honorar'
? eurFormat(wizardHonorar()) + ' Honorar'
: 'Projektgröße ' + faktorFormat(wizardGroesse()))}
${zeile('Honorar', wizardHonorar() > 0 ? eurFormat(wizardHonorar()) : 'nicht angegeben', !(wizardHonorar() > 0))}
${zeile('Beauftragte Phasen', esc(wizard.beauftragt.join(', ')))}
${zeile('Amtierende Phase', esc(wizard.amtierend))}
${zeile('Team', esc(team))}
@@ -547,14 +505,12 @@ function wizardPruefenHtml(){
const p = wizardPersonen().find(x => x.id === id);
return p ? p.kuerzel + (p.gewerk ? ' (' + p.gewerk + ')' : '') : id;
}).join(', '))) : ''}
${zeile('Terminplan', wizard.grundlage !== 'honorar'
? 'nicht möglich — Grundlage ist die Projektgröße'
${zeile('Terminplan', !(wizardHonorar() > 0)
? 'nicht möglich — kein Honorar angegeben'
: (!wizard.planErzeugen ? 'wird nicht erzeugt'
: (plan.length ? plan.length + ' Phasen bis ' + formatWeekDate(isoVon(plan[plan.length - 1].ende)) : 'nicht errechenbar')),
wizard.grundlage !== 'honorar' || !wizard.planErzeugen || !plan.length)}
</div>
<p class="hint" style="margin-top:14px">Die Gewichtung des Aufwands je Phase und Rolle übernimmt die
firmenweite Vorlage; sie lässt sich später im Projektbereich anpassen.</p>`;
!(wizardHonorar() > 0) || !wizard.planErzeugen || !plan.length)}
</div>`;
}
function wizardAbschlussHtml(){
@@ -585,13 +541,8 @@ function wizardSetz(feld, wert){
if(wert === 'anfrage' || wert === 'angebot') wizard.gruppe = 'In Anbahnung';
else if(wizard.gruppe === 'In Anbahnung') wizard.gruppe = 'Aktive Projekte';
}
if(feld === 'grundlage'){
// Das nun gesperrte Feld leeren: Ein Wert, der dasteht und nicht mehr
// zählt, führt in die Irre.
if(wert === 'honorar') wizard.groesse = ''; else wizard.honorar = '';
}
// Freitextfelder nicht neu bauen — sonst springt der Cursor ans Feldende.
if(feld === 'nr' || feld === 'name' || feld === 'honorar' || feld === 'groesse' || feld === 'planPersonen'){
if(feld === 'nr' || feld === 'name' || feld === 'honorar' || feld === 'planPersonen'){
wizardNurAbhaengiges();
return;
}
@@ -624,8 +575,6 @@ function wizardNurAbhaengiges(){
const hh = document.getElementById('wizHinweisHonorar');
if(hh) hh.textContent = wizardHinweisHonorar();
const hg = document.getElementById('wizHinweisGroesse');
if(hg) hg.textContent = wizardHinweisGroesse();
const tab = document.getElementById('wizPlanTabelle');
if(tab) tab.innerHTML = wizardPlanTabelleHtml();
@@ -725,8 +674,7 @@ function wizardAnlegen(){
status: '', statusState: undefined, order: nextOrder,
lph: {}, assign: {}, plan: null,
auftragsstatus: auftragsstatusOf({ auftragsstatus: wizard.status }),
honorar: wizard.grundlage === 'honorar' ? wizardHonorar() : 0,
groesse: wizard.grundlage === 'groesse' ? wizardGroesse() : 0,
honorar: wizardHonorar(),
beauftragteLP: wizard.beauftragt.slice(),
aktuelleLP: [wizard.amtierend],
};
@@ -744,13 +692,13 @@ function wizardAnlegen(){
todos: zeilen.map((z, i) => ({
id: uid(),
title: z.lph,
lph: z.lph,
start: isoVon(z.start),
end: isoVon(z.ende),
done: false,
progress: 0,
assignees: [],
deps: [],
order: i,
dependsOn: [],
})),
milestones: [],
};