Aufgaben im Wochenbericht, Meilensteine „Erreicht“, Panels als Planblatt

- Wochenbericht: Abschnitt „Aufgaben“ je Projekt (erledigt seit Vorwoche,
  überfällig, nächste zwei Wochen), rote Zahl in der Tagesordnung,
  Tabelle in der PDF-Akte; Klick öffnet das Aufgaben-Panel
- Meilensteine lassen sich als erreicht markieren (plan_milestones.erreicht);
  nur nicht erreichte vergangene gelten als überfällig
- Aufgabe- und Meilenstein-Panel neu aufgebaut: Stand oben, Lage im
  Terminplan, Angaben als Zeilen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
erikandClaude Opus 5.5 committed 2026-11-10 10:41:44 +01:00
1 parent 4ac3d8d53d
commit e12936444d
13 files changed
+693 -137

No files matched your search

+144 -107
View File
@@ -38,8 +38,8 @@
<link rel="icon" type="image/png" href="/logo.png">
<link rel="alternate icon" href="/logo.ico">
<link rel="apple-touch-icon" href="/logo.png">
<link rel="stylesheet" href="/styles.css?v=123">
<link rel="stylesheet" href="/plan-styles.css?v=56">
<link rel="stylesheet" href="/styles.css?v=124">
<link rel="stylesheet" href="/plan-styles.css?v=57">
<!-- Glas-Darstellung (public/glas.css, als letztes Stylesheet): greift nur mit
der Klasse html.glas. Gesetzt wird sie hier, vor dem ersten Zeichnen,
damit die Seite nicht erst ohne Glas aufblitzt; abschaltbar je Browser
@@ -1502,41 +1502,67 @@
Art und Titel, Abschnitte Wann / Wo bzw. Wer / Stand / Hängt ab von,
feste Fußleiste. Alle Feld-ids wie zuvor; befüllt und ausgewertet von
js/plan/plan-modals.js. Stile: Block "Seitenpanel" in plan-styles.css. -->
<!-- Seitenpanels Aufgabe und Meilenstein als "Planblatt" (Eriks Wahl,
unslop): Kopf mit Art, Projekt und Stand, darunter die Lage im
Terminplan (todoLageNeu()/msLageBildNeu(), js/plan/plan-modals.js), dann
die Angaben als Zeilen mit Beschriftung. Alle Feld-ids wie zuvor.
Stile: Block "Planblatt" am Ende von plan-styles.css. -->
<div class="modal-bg als-panel" id="milestoneModal" style="display:none">
<div class="modal pd">
<div class="pd-kopf">
<div class="pd-art"><span class="pd-raute" aria-hidden="true"></span><span id="milestoneModalTitle">Meilenstein</span>
<span class="modal-kbd">Esc schließt</span>
<button type="button" class="pd-zu" onclick="closeMilestoneModal()" aria-label="Schließen">×</button></div>
<span class="pd-wo" id="milestoneModalWo"></span>
<span class="modal-kbd"><kbd>Esc</kbd></span>
<button type="button" class="pd-zu" onclick="closeMilestoneModal()" aria-label="Schließen"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" aria-hidden="true"><line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/></svg></button></div>
<input type="text" id="milestoneEditName" class="pd-titel" placeholder="z. B. Abgabe Genehmigungsplanung" aria-label="Name des Meilensteins">
<div class="pd-stand">
<div class="segmented pd-seg" id="milestoneEditStand" role="group" aria-label="Stand">
<button type="button" data-stand="offen" aria-pressed="true" onclick="msStandSetzen(false)"><span class="segmented-dot"></span>Offen</button>
<button type="button" data-stand="erreicht" aria-pressed="false" onclick="msStandSetzen(true)"><span class="segmented-dot"></span>Erreicht</button>
</div>
<input type="checkbox" id="milestoneEditErreicht" hidden>
<span class="pd-warnung" id="milestoneEditWarnung" hidden></span>
</div>
<div class="pd-lage" id="msLageBild" aria-label="Lage im Terminplan"></div>
</div>
<div class="pd-inhalt">
<section class="pd-abs">
<h3>Wann</h3>
<!-- sichtbares Anzeigefeld (deutsches Format) + verstecktes ISO-Feld -->
<input type="text" id="milestoneEditDateDisplay" class="range-picker-display" placeholder="Datum wählen…" readonly aria-label="Datum"
onclick="openDatePicker({ anchor: this, inputId: 'milestoneEditDate', displayId: 'milestoneEditDateDisplay' })">
<input type="hidden" id="milestoneEditDate">
</section>
<section class="pd-abs">
<h3>Wo im Terminplan</h3>
<div class="segmented pd-seg" id="milestoneEditLage">
<button type="button" data-lage="zeile" aria-pressed="false" onclick="msLageSetzen('zeile')">Oben, eigene Zeile</button>
<button type="button" data-lage="aufgabe" aria-pressed="false" onclick="msLageSetzen('aufgabe')">Bei einer Aufgabe</button>
<div class="pd-zeile">
<span class="pd-lab">Datum</span>
<div class="pd-wert">
<!-- sichtbares Anzeigefeld (deutsches Format) + verstecktes ISO-Feld -->
<div class="field-inline">
<input type="text" id="milestoneEditDateDisplay" class="range-picker-display" placeholder="Datum wählen…" readonly aria-label="Datum"
onclick="openDatePicker({ anchor: this, inputId: 'milestoneEditDate', displayId: 'milestoneEditDateDisplay', onApply: msLageBildNeu })">
<span class="field-badge pd-kw" id="milestoneEditKw" hidden></span>
</div>
<input type="hidden" id="milestoneEditDate">
</div>
<select id="milestoneEditPosition" aria-label="Position bei einer Aufgabe" onchange="msLageSync()">
<option value="">Oben in der Projektzeile</option>
</select>
</section>
<section class="pd-abs">
<h3>Hängt ab von</h3>
<div id="milestoneEditDependsOn" class="todo-depends-list pd-liste"></div>
</section>
<section class="pd-abs">
<h3>Notiz</h3>
<p class="hint pd-hint pd-mail-hinweis" id="milestoneEditMailHinweis" hidden></p>
<textarea id="milestoneEditNotiz" class="pd-notiz" rows="4" placeholder="Optional, z. B. Hintergrund oder die Mail, aus der der Meilenstein stammt" aria-label="Notiz"></textarea>
</section>
</div>
<div class="pd-zeile">
<span class="pd-lab">Im Terminplan</span>
<div class="pd-wert">
<div class="segmented pd-seg" id="milestoneEditLage">
<button type="button" data-lage="zeile" aria-pressed="false" onclick="msLageSetzen('zeile')">Eigene Zeile oben</button>
<button type="button" data-lage="aufgabe" aria-pressed="false" onclick="msLageSetzen('aufgabe')">Bei einer Aufgabe</button>
</div>
<select id="milestoneEditPosition" aria-label="Position bei einer Aufgabe" onchange="msLageSync()">
<option value="">Oben in der Projektzeile</option>
</select>
</div>
</div>
<div class="pd-zeile">
<span class="pd-lab">Hängt ab von</span>
<div class="pd-wert">
<div id="milestoneEditDependsOn" class="todo-depends-list pd-liste" onchange="msLageBildNeu()"></div>
</div>
</div>
<div class="pd-zeile">
<span class="pd-lab"><label for="milestoneEditNotiz">Notiz</label></span>
<div class="pd-wert">
<p class="hint pd-hint pd-mail-hinweis" id="milestoneEditMailHinweis" hidden></p>
<textarea id="milestoneEditNotiz" class="pd-notiz" rows="3" placeholder="Hintergrund oder die Mail, aus der der Meilenstein stammt"></textarea>
</div>
</div>
</div>
<div class="pd-fuss">
<button type="button" class="pd-loeschen" id="milestoneEditDelete" onclick="deleteMilestoneEdit()">Löschen</button>
@@ -1549,51 +1575,69 @@
<div class="modal pd">
<div class="pd-kopf">
<div class="pd-art"><span class="pd-kasten" aria-hidden="true"></span><span id="todoModalTitle">Aufgabe bearbeiten</span>
<span class="modal-kbd" id="todoModalKbd">Esc · Strg+↵</span>
<button type="button" class="pd-zu" onclick="closeTodoModal()" aria-label="Schließen">×</button></div>
<span class="pd-wo" id="todoModalWo"></span>
<span class="modal-kbd" id="todoModalKbd"><kbd>Esc</kbd> <kbd>Strg</kbd><kbd>↵</kbd></span>
<button type="button" class="pd-zu" onclick="closeTodoModal()" aria-label="Schließen"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" aria-hidden="true"><line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/></svg></button></div>
<input type="text" id="todoEditTitle" class="pd-titel" placeholder="Titel der Aufgabe" aria-label="Titel">
<span class="field-error" id="todoEditTitleError" hidden></span>
<div class="pd-stand">
<div class="segmented pd-seg" id="todoEditStatus">
<button type="button" data-status="offen" aria-pressed="false" onclick="setTodoEditStatus('offen')"><span class="segmented-dot"></span>Offen</button>
<button type="button" data-status="arbeit" aria-pressed="false" onclick="setTodoEditStatus('arbeit')"><span class="segmented-dot"></span>In Arbeit</button>
<button type="button" data-status="fertig" aria-pressed="false" onclick="setTodoEditStatus('fertig')"><span class="segmented-dot"></span>Erledigt</button>
</div>
<div class="progress-row" id="todoEditProgressRow">
<input type="range" id="todoEditProgress" min="0" max="100" step="5" value="0" aria-label="Fortschritt"
oninput="onTodoProgressInput(this)">
<output id="todoEditProgressVal">0 %</output>
</div>
</div>
<div class="pd-lage" id="todoLageBild" aria-label="Lage im Terminplan"></div>
</div>
<div class="pd-inhalt">
<!-- Nur im Phasen-Modus ("+ Neu" → "Leistungsphase" oder Bearbeiten
eines Phasenbalkens): Welche Leistungsphase dieser Balken ist. Nur
darüber wird ein Balken zum Phasenkopf, nicht mehr über den Titel. -->
<section class="pd-abs" id="todoEditPhaseAbs" hidden>
<h3>Leistungsphase</h3>
<select id="todoEditLph" aria-label="Leistungsphase" onchange="todoPhaseGewechselt()"></select>
<p class="hint pd-hint">Aufgaben, die im Zeitraum dieser Phase beginnen, stehen im Terminplan darunter.</p>
</section>
<section class="pd-abs">
<h3>Wann</h3>
<div class="field-inline">
<input type="text" id="todoEditRange" class="range-picker-display" placeholder="Start – Ende wählen…" readonly aria-label="Zeitraum"
onclick="openRangePicker({ anchor: this, startInputId: 'todoEditStart', endInputId: 'todoEditEnd', displayId: 'todoEditRange', onApply: refreshTodoDuration })">
<span class="field-badge" id="todoEditDuration" hidden></span>
<div class="pd-zeile" id="todoEditPhaseAbs" hidden>
<span class="pd-lab"><label for="todoEditLph">Leistungsphase</label></span>
<div class="pd-wert">
<select id="todoEditLph" onchange="todoPhaseGewechselt()"></select>
<p class="hint pd-hint">Aufgaben, die im Zeitraum dieser Phase beginnen, stehen im Terminplan darunter.</p>
</div>
<input type="hidden" id="todoEditStart">
<input type="hidden" id="todoEditEnd">
<span class="field-error" id="todoEditRangeError" hidden></span>
<label class="pd-haken"><input type="checkbox" id="todoEditShowInGantt" checked> Im Terminplan als Balken zeigen</label>
<p class="hint pd-hint">Ohne Zeitraum erscheint die Aufgabe nur unter „Aufgaben“, nicht im Terminplan.</p>
</section>
<section class="pd-abs" id="todoEditWerAbs">
<h3>Wer</h3>
<div id="todoEditAssignees" class="person-chips"></div>
<!-- Externe Personen: Kollegen mit Konto, die dem Projekt NICHT
zugeordnet sind (Eriks Wunsch). todoExternHinzufuegen()/
todoExternEntfernen(), js/plan/plan-modals.js -->
<div class="pd-externe">
<div class="pd-externe-kopf">Externe Personen <span class="hint">nicht im Projekt</span></div>
<div id="todoEditExterne" class="person-chips"></div>
<select id="todoEditExterneWahl" aria-label="Externe Person hinzufügen" onchange="todoExternHinzufuegen(this.value)"></select>
</div>
<!-- Aufwand: −/+, Schnellwahl, Umrechnung in Arbeitstage
(todoAufwandAendern()/todoAufwandSetzen()/todoAufwandInfo(), plan-modals.js) -->
<div class="pd-aufwand">
<div class="pd-aufwand-kopf">
<label for="todoEditAufwand">Aufwand</label>
<span>optional · zählt zur Auslastung der Zuständigen</span>
</div>
<div class="pd-zeile">
<span class="pd-lab">Zeitraum</span>
<div class="pd-wert">
<div class="field-inline">
<input type="text" id="todoEditRange" class="range-picker-display" placeholder="Start – Ende wählen…" readonly aria-label="Zeitraum"
onclick="openRangePicker({ anchor: this, startInputId: 'todoEditStart', endInputId: 'todoEditEnd', displayId: 'todoEditRange', onApply: refreshTodoDuration })">
<span class="field-badge" id="todoEditDuration" hidden></span>
</div>
<input type="hidden" id="todoEditStart">
<input type="hidden" id="todoEditEnd">
<span class="field-error" id="todoEditRangeError" hidden></span>
<label class="pd-haken"><input type="checkbox" id="todoEditShowInGantt" checked> Als Balken im Terminplan</label>
</div>
</div>
<div class="pd-zeile" id="todoEditWerAbs">
<span class="pd-lab">Zuständig</span>
<div class="pd-wert">
<div id="todoEditAssignees" class="person-chips"></div>
<!-- Externe Personen: Kollegen mit Konto, die dem Projekt NICHT
zugeordnet sind (Eriks Wunsch). todoExternHinzufuegen()/
todoExternEntfernen(), js/plan/plan-modals.js -->
<div class="pd-externe">
<div class="pd-externe-kopf">Externe Personen <span class="hint">nicht im Projekt</span></div>
<div id="todoEditExterne" class="person-chips"></div>
<select id="todoEditExterneWahl" aria-label="Externe Person hinzufügen" onchange="todoExternHinzufuegen(this.value)"></select>
</div>
</div>
</div>
<!-- Aufwand: −/+, Schnellwahl, Umrechnung in Arbeitstage
(todoAufwandAendern()/todoAufwandSetzen()/todoAufwandInfo(), plan-modals.js) -->
<div class="pd-zeile" id="todoEditAufwandAbs">
<span class="pd-lab"><label for="todoEditAufwand">Aufwand</label></span>
<div class="pd-wert pd-aufwand">
<div class="pd-aufwand-zeile">
<div class="pd-stepper">
<button type="button" class="pd-stepper-knopf" onclick="todoAufwandAendern(-1)" aria-label="Eine Stunde weniger">−</button>
@@ -1611,41 +1655,34 @@
</div>
<span class="pd-aufwand-info" id="todoAufwandInfoText"></span>
</div>
</section>
<section class="pd-abs">
<h3>Stand</h3>
<div class="segmented pd-seg" id="todoEditStatus">
<button type="button" data-status="offen" aria-pressed="false" onclick="setTodoEditStatus('offen')"><span class="segmented-dot"></span>Offen</button>
<button type="button" data-status="arbeit" aria-pressed="false" onclick="setTodoEditStatus('arbeit')"><span class="segmented-dot"></span>In Arbeit</button>
<button type="button" data-status="fertig" aria-pressed="false" onclick="setTodoEditStatus('fertig')"><span class="segmented-dot"></span>Erledigt</button>
</div>
<div class="progress-row" id="todoEditProgressRow">
<input type="range" id="todoEditProgress" min="0" max="100" step="5" value="0" aria-label="Fortschritt"
oninput="onTodoProgressInput(this)">
<output id="todoEditProgressVal">0 %</output>
</div>
</section>
<section class="pd-abs">
<h3>Notiz</h3>
<p class="hint pd-hint pd-mail-hinweis" id="todoEditMailHinweis" hidden></p>
<textarea id="todoEditNotiz" class="pd-notiz" rows="4" placeholder="Optional, z. B. Hintergrund oder die Mail, aus der die Aufgabe stammt" aria-label="Notiz"></textarea>
</section>
<section class="pd-abs">
<details class="fold" id="todoEditDependsFold">
<summary>
<span class="fold-caret">▶</span>
<span>Hängt ab von</span>
<span class="fold-count zero" id="todoEditDependsCount">0</span>
</summary>
<div class="fold-body">
<div class="fold-search">
<span class="fold-search-icon">⌕</span>
<input type="text" id="todoEditDependsSearch" placeholder="Aufgabe oder Meilenstein suchen…" oninput="filterTodoDepends()">
</div>
<div class="pd-zeile">
<span class="pd-lab">Hängt ab von</span>
<div class="pd-wert">
<div class="pd-vorg" id="todoEditDependsSummary"></div>
<details class="fold" id="todoEditDependsFold">
<summary>
<span class="fold-caret"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 6 6 6-6 6"/></svg></span>
<span>Vorgänger ändern</span>
<span class="fold-count zero" id="todoEditDependsCount">0</span>
</summary>
<div class="fold-body">
<div class="fold-search">
<span class="fold-search-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" aria-hidden="true"><circle cx="11" cy="11" r="6"/><line x1="15.5" y1="15.5" x2="20" y2="20"/></svg></span>
<input type="text" id="todoEditDependsSearch" placeholder="Aufgabe oder Meilenstein suchen" oninput="filterTodoDepends()">
</div>
<div id="todoEditDependsOn" class="todo-depends-list flat" onchange="refreshTodoDependsCount()"></div>
</div>
<div id="todoEditDependsOn" class="todo-depends-list flat" onchange="refreshTodoDependsCount()"></div>
</div>
</details>
</section>
</details>
</div>
</div>
<div class="pd-zeile">
<span class="pd-lab"><label for="todoEditNotiz">Notiz</label></span>
<div class="pd-wert">
<p class="hint pd-hint pd-mail-hinweis" id="todoEditMailHinweis" hidden></p>
<textarea id="todoEditNotiz" class="pd-notiz" rows="3" placeholder="Hintergrund oder die Mail, aus der die Aufgabe stammt"></textarea>
</div>
</div>
</div>
<div class="pd-fuss">
<button type="button" class="pd-loeschen" id="todoEditDelete" onclick="deleteTodoEdit()">Löschen</button>
@@ -1963,7 +2000,7 @@
<script src="/js/store.js?v=17"></script>
<script src="/js/weeks.js?v=12"></script>
<script src="/js/board/board.js?v=42"></script>
<script src="/js/wochenbericht/wochenbericht.js?v=29"></script>
<script src="/js/wochenbericht/wochenbericht.js?v=31"></script>
<script src="/js/people/people.js?v=8"></script>
<script src="/js/planung/planung.js?v=3"></script>
<script src="/js/planchecker/planchecker.js?v=1"></script>
@@ -1977,7 +2014,7 @@
<!-- Assistent "Neues Projekt": braucht auftrag.js (Auftragsstatus) und
load.js (faktorFormat), steht deshalb dahinter. -->
<script src="/js/projekt-wizard.js?v=6"></script>
<script src="/js/dashboard/dashboard.js?v=13"></script>
<script src="/js/dashboard/dashboard.js?v=14"></script>
<script src="/js/benachrichtigungen.js?v=5"></script>
<!-- wand.js braucht berechneAuslastung()/loadAmpelColor()/kapazitaetsFaktor()/
stundenFormat() (load.js), classifyProjectChange()/findPreviousWeek()
@@ -1985,7 +2022,7 @@
relativerTag()/tageBis()/initialsFor() (dashboard.js) — alle laden
bereits vorher, die Reihenfolge hier ist also nur Zuordnung zum
Tab-Ordner, keine neue Abhängigkeit nach vorn. -->
<script src="/js/wand/wand.js?v=21"></script>
<script src="/js/wand/wand.js?v=22"></script>
<script src="/js/settings/settings.js?v=14"></script>
<script src="/js/admin/admin.js?v=24"></script>
<script src="/js/admin-users.js?v=10"></script>
@@ -2000,14 +2037,14 @@
js/gantt.js kommen, dessen buildGanttHtml() plan-build-gantt.js
umhuellt. -->
<script src="/js/plan/plan-state.js?v=25"></script>
<script src="/js/plan/plan-sections.js?v=12"></script>
<script src="/js/plan/plan-sections.js?v=13"></script>
<script src="/js/plan/plan-info.js?v=17"></script>
<script src="/js/plan/plan-history.js?v=4"></script>
<script src="/js/plan/plan-build-gantt.js?v=11"></script>
<script src="/js/plan/plan-gantt.js?v=23"></script>
<script src="/js/plan/plan-gantt.js?v=24"></script>
<script src="/js/plan/plan-drag.js?v=8"></script>
<script src="/js/plan/plan-modals.js?v=18"></script>
<script src="/js/plan/plan-tasks.js?v=4"></script>
<script src="/js/plan/plan-modals.js?v=21"></script>
<script src="/js/plan/plan-tasks.js?v=5"></script>
<script src="/js/plan/plan-phasen.js?v=3"></script>
<script src="/js/plan/plan-files.js?v=4"></script>
<script src="/js/plan/plan-protokoll.js?v=21"></script>
@@ -2023,7 +2060,7 @@
<script src="/js/plan/plan-settings.js?v=14"></script>
<script src="/js/plan/plan-schedule.js?v=7"></script>
<script src="/js/tabs-auth.js?v=19"></script>
<script src="/js/pdf.js?v=31"></script>
<script src="/js/pdf.js?v=32"></script>
<script src="/js/backup.js?v=2"></script>
<script src="/js/import.js?v=1"></script>
<script src="/js/init.js?v=14"></script>
+5 -5
View File
@@ -212,14 +212,14 @@ function dashDaten(){
});
(plan.milestones || []).forEach(m => {
if(!m.date) return;
termine.push({ art: 'meilenstein', titel: m.name || 'Meilenstein', datum: m.date, meins: false, wer: [] });
termine.push({ art: 'meilenstein', titel: m.name || 'Meilenstein', datum: m.date, meins: false, wer: [], erreicht: !!m.erreicht });
});
const hatEigene = termine.some(t => t.meins);
if(!rollen.length && !hatEigene) return;
// Erledigte Vergangenheit interessiert hier nicht: Meilensteine vor heute
// fallen weg (sie haben kein Erledigt-Kennzeichen), offene Aufgaben vor
// heute bleiben als überfällig stehen.
const relevant = termine.filter(t => !(t.art === 'meilenstein' && t.datum < heute));
// Erreichte Meilensteine fallen weg (im Kalender bleiben sie stehen);
// nicht erreichte vor heute gelten wie offene Aufgaben als überfällig —
// dieselbe Regel wie die Zahl im Band (GET /my-dashboard).
const relevant = termine.filter(t => !(t.art === 'meilenstein' && t.erreicht));
relevant.sort((a, b) => (a.datum || '9999').localeCompare(b.datum || '9999'));
projekte.push({ p, rollen, termine: relevant, alleTermine: termine });
});
+31 -2
View File
@@ -518,13 +518,14 @@ function pdfWbProjektAkte(p, w, prevWeek, allePersonen){
color: '#D70015', margin: [0, 6, 0, 0] });
}
const innerContent = {
const aufgaben = pdfWbAufgaben(p, w, prevWeek);
const innerContent = { stack: [{
columns: [
{ width: '*', stack: [kopf, aenderung, kommentar].concat(vorher ? [vorher] : []) },
{ width: 190, stack: rightStack }
],
columnGap: 12
};
}].concat(aufgaben ? [aufgaben] : []) };
return {
table: {
@@ -548,6 +549,34 @@ function pdfWbProjektAkte(p, w, prevWeek, allePersonen){
};
}
// pdfWbAufgaben: die Aufgaben der Akte wie auf dem Bildschirm
// (berichtAufgabenDaten() in wochenbericht.js) — erledigt seit der Vorwoche,
// überfällig, nächste zwei Wochen, je höchstens fünf. Rückgabe: pdfmake-Block
// oder null, wenn in keiner Gruppe etwas steht.
function pdfWbAufgaben(p, w, prevWeek){
if(typeof berichtAufgabenDaten !== 'function') return null;
const d = berichtAufgabenDaten(p, w, prevWeek);
const gruppen = [
[prevWeek ? 'Erledigt seit ' + berichtKurzKw(prevWeek.label) : 'Erledigt', d.erledigt, null],
['Überfällig', d.ueberfaellig, '#D70015'],
['Nächste zwei Wochen', d.bald, null]
].filter(g => g[1].length);
if(!gruppen.length) return null;
const body = [];
gruppen.forEach(([titel, liste, farbe]) => {
liste.slice(0, 5).forEach((e, i) => body.push([
{ text: i === 0 ? titel + ' (' + liste.length + ')' : '', style: 'td', bold: true, color: farbe || undefined },
{ text: (e.art === 'meilenstein' ? 'Meilenstein: ' : '') + e.titel + (e.neu ? ' neu' : ''), style: 'td' },
{ text: e.kuerzel, style: 'td' },
{ text: e.datum ? formatDateDE(parseDateISO(e.datum)) : '', style: 'td', alignment: 'right', color: farbe || undefined }
]));
});
return { stack: [
{ text: 'AUFGABEN', fontSize: 6.5, bold: true, color: '#8E8E93', margin: [0, 10, 0, 3] },
{ table: { widths: [100, '*', 70, 50], body }, layout: pdfWbGridLayout(false) }
] };
}
// pdfWbVorigerKommentar: Block mit dem jüngsten früheren Kommentar des
// Gewerks. Steht er in der direkten Vorwoche, heißt er "Vorwoche KW …",
// sonst "Zuletzt KW …". Parameter: p (Projekt), w (Sitzung). Rückgabe:
+1 -1
View File
@@ -171,7 +171,7 @@
const openSub = overdue.length > 0 ? overdue.length + ' überfällig' : (open.length > 0 ? 'im Zeitplan' : 'alle erledigt');
const upcoming = milestones
.filter(m => m.date && parseDateISO(m.date) >= today)
.filter(m => m.date && !m.erreicht && parseDateISO(m.date) >= today)
.sort((a, b) => parseDateISO(a.date) - parseDateISO(b.date));
const msZelle = upcoming.length > 0
? bandZelle('Nächster Meilenstein', formatDateDE(parseDateISO(upcoming[0].date)), upcoming[0].name || 'Meilenstein')
+164 -3
View File
@@ -65,6 +65,9 @@
document.getElementById('milestoneEditDate').value = msDateIso;
const msDateDisplay = document.getElementById('milestoneEditDateDisplay');
if(msDateDisplay) msDateDisplay.value = msDateIso ? formatDateDE(parseDateISO(msDateIso)) : '';
const msErreicht = document.getElementById('milestoneEditErreicht');
if(msErreicht) msErreicht.checked = !isNew && !!currentMs.erreicht;
panelWoSetzen('milestoneModalWo', '');
const positionSelect = document.getElementById('milestoneEditPosition');
if(positionSelect){
@@ -129,6 +132,7 @@
dependsOnList.innerHTML = listHtml;
}
msLageBildNeu();
document.getElementById('milestoneModal').style.display = 'flex';
document.getElementById('milestoneEditName').focus();
};
@@ -167,6 +171,7 @@
window.closeMilestoneModal = function(){
document.getElementById('milestoneModal').style.display = 'none';
app.currentMilestoneIndex = null;
if(currentTab === 'wochenbericht') berichtAufgabenNeu();
const danach = msNachSchliessen;
msNachSchliessen = null;
if(danach) setTimeout(danach, 0);
@@ -189,12 +194,14 @@
const position = positionSelect ? positionSelect.value : '';
const msNotizEl = document.getElementById('milestoneEditNotiz');
const notiz = msNotizEl ? msNotizEl.value.trim() : '';
const msErreichtEl = document.getElementById('milestoneEditErreicht');
const erreicht = !!(msErreichtEl && msErreichtEl.checked);
const plan = app.ensurePlanLocal();
if(!Array.isArray(plan.milestones)) plan.milestones = [];
if(app.currentMilestoneIndex === null){
plan.milestones.push({ id: uid(), name, date, dependsOn, position, notiz });
plan.milestones.push({ id: uid(), name, date, dependsOn, position, notiz, erreicht });
} else if(plan.milestones[app.currentMilestoneIndex]){
const m = plan.milestones[app.currentMilestoneIndex];
m.name = name;
@@ -202,6 +209,7 @@
m.dependsOn = dependsOn;
m.position = position;
m.notiz = notiz;
m.erreicht = erreicht;
}
plan.milestones.sort((a,b) => parseDateISO(a.date) - parseDateISO(b.date));
app.savePlan();
@@ -266,6 +274,140 @@
return progress > 0 ? 'arbeit' : 'offen';
}
// ---------- Planblatt: Kopfzeile, Lage im Terminplan, Stand ----------
// kurzDatum: "2026-11-19" -> "19.11." Parameter: iso. Rückgabe: String.
function kurzDatum(iso){ return formatDateDE(parseDateISO(iso)).slice(0, 6); }
// planKnoten: Aufgabe oder Meilenstein zu einer Knoten-id (Meilensteine mit
// Präfix "m_", wie in dependsOn). Rückgabe: {titel, start, end, ms, fertig} oder null.
function planKnoten(plan, id){
id = String(id || '');
if(id.indexOf('m_') === 0){
const m = (plan.milestones || []).find(x => 'm_' + x.id === id);
return m ? { id, titel: m.name || 'Meilenstein', start: m.date || '', end: m.date || '', ms: true, fertig: !!m.erreicht } : null;
}
const t = (plan.todos || []).find(x => String(x.id) === id);
return t ? { id, titel: t.title || 'Aufgabe', start: t.start || t.end || '', end: t.end || t.start || '', ms: false, fertig: !!t.done } : null;
}
// panelWoSetzen: Projekt (Nummer, Name) und optional Phase in der Kopfzeile.
function panelWoSetzen(elId, phase){
const el = document.getElementById(elId);
if(!el) return;
const p = app.project || {};
el.innerHTML = (p.nr ? `<span class="pd-nr">${esc(p.nr)}</span> ` : '') + esc(p.name || '') + (phase ? ' · ' + esc(phase) : '');
}
// LAGE_TAGE: der Ausschnitt im Panelkopf — sechs Kalenderwochen ab dem
// Montag der Woche VOR dem Eintrag.
const LAGE_TAGE = 42;
// lageBildHtml: Ausschnitt des Terminplans um einen Eintrag. Oben die
// Vorgänger (grau, wenn erledigt), in der Mitte der Eintrag selbst, unten
// die Nachfolger; dazu die Heute-Linie. Nur Anzeige.
// e = {id (Knoten-id oder null), start, end, ms, progress, fertig, deps}.
// Rückgabe: HTML.
function lageBildHtml(e){
const plan = app.ensurePlanLocal();
const s = parseDateISO(e.start || e.end), en = parseDateISO(e.end || e.start);
if(!s || !en) return '<div class="pd-lage-leer">Noch kein Termin gewählt</div>';
const vor = (e.deps || []).map(id => planKnoten(plan, id)).filter(k => k && k.start);
// Ausschnitt beginnt eine Woche vor dem Eintrag, früher, wenn ein
// Vorgänger davor endet (höchstens drei Wochen), damit er sichtbar bleibt.
const mo = new Date(s);
mo.setDate(mo.getDate() - 7);
const grenze = new Date(s); grenze.setDate(grenze.getDate() - 21);
vor.forEach(k => { const v = parseDateISO(k.end); if(v && v < mo) mo.setTime(Math.max(v.getTime() - 2 * 864e5, grenze.getTime())); });
mo.setDate(mo.getDate() - ((mo.getDay() + 6) % 7));
const tag = 100 / LAGE_TAGE;
const x = d => daysBetween(mo, d) * tag;
const nach = e.id ? (plan.todos || []).filter(t => (t.dependsOn || []).includes(e.id)).map(t => planKnoten(plan, t.id))
.concat((plan.milestones || []).filter(m => (m.dependsOn || []).includes(e.id)).map(m => planKnoten(plan, 'm_' + m.id)))
.filter(k => k && k.start) : [];
const form = (k, y, klasse, progress) => {
const a = parseDateISO(k.start), b = parseDateISO(k.end) || a;
if(k.ms){
const l = x(a) + tag / 2;
return l < 0 || l > 100 ? '' : `<span class="pd-lage-raute ${klasse}" style="left:${l}%;top:${y}px" title="${esc(k.titel)}"></span>`;
}
const l = Math.max(0, x(a)), r = Math.min(100, x(b) + tag);
if(r <= l) return '';
return `<span class="pd-lage-bar ${klasse}" style="left:${l}%;width:${r - l}%;top:${y}px" title="${esc(k.titel)}">`
+ (progress > 0 ? `<b style="width:${Math.min(100, progress)}%"></b>` : '') + '</span>';
};
const ich = { titel: '', start: e.start || e.end, end: e.end || e.start, ms: e.ms };
const ichKlasse = 'is-diese' + (e.fertig ? ' is-fertig' : '') + (e.ueber ? ' is-ueber' : '');
let flaeche = vor.slice(0, 3).map(k => form(k, 4, k.fertig ? 'is-fertig' : 'is-vor', 0)).join('')
+ form(ich, e.ms ? 18 : 20, ichKlasse, e.ms ? 0 : e.progress)
+ nach.slice(0, 3).map(k => form(k, 36, 'is-nach', 0)).join('');
const heute = parseDateISO(heuteIso());
const hx = x(heute) + tag / 2;
if(hx >= 0 && hx <= 100) flaeche += `<span class="pd-lage-heute" style="left:${hx}%"></span>`;
const skala = [0, 1, 2, 3, 4, 5].map(i => {
const d = new Date(mo); d.setDate(d.getDate() + i * 7);
return `<span style="left:${i * 100 / 6}%">KW ${String(getWeekNumber(d)).padStart(2, '0')}</span>`;
}).join('');
const legende = [];
if(vor[0]) legende.push(`<span>${esc(vor[0].titel)}${vor[0].fertig ? ', erledigt' : ''}</span>`);
if(hx >= 0 && hx <= 100) legende.push(`<span>heute ${esc(kurzDatum(heuteIso()))}</span>`);
if(nach[0]) legende.push(`<span>danach ${esc(nach[0].titel)} ${esc(kurzDatum(nach[0].start))}</span>`);
return `<div class="pd-lage-skala">${skala}</div><div class="pd-lage-flaeche">${flaeche}</div>`
+ (legende.length ? `<div class="pd-lage-legende">${legende.join('')}</div>` : '');
}
// todoLageNeu: zeichnet die Lage der Aufgabe aus den aktuellen Eingaben.
window.todoLageNeu = function(){
const el = document.getElementById('todoLageBild');
if(!el || !app.project) return;
const plan = app.ensurePlanLocal();
const t = app.currentTodoIndex !== null ? plan.todos[app.currentTodoIndex] : null;
const status = currentTodoEditStatus();
const slider = document.getElementById('todoEditProgress');
el.innerHTML = lageBildHtml({
id: t ? t.id : null,
start: document.getElementById('todoEditStart').value,
end: document.getElementById('todoEditEnd').value,
ms: false, fertig: status === 'fertig',
progress: status === 'fertig' ? 100 : (status === 'arbeit' && slider ? Number(slider.value) : 0),
deps: Array.from(document.querySelectorAll('#todoEditDependsOn input[type="checkbox"]:checked')).map(cb => cb.value)
});
};
// msLageBildNeu: Lage, KW-Marke und Überfällig-Hinweis des Meilensteins.
window.msLageBildNeu = function(){
const el = document.getElementById('msLageBild');
if(!el || !app.project) return;
const plan = app.ensurePlanLocal();
const m = app.currentMilestoneIndex !== null ? plan.milestones[app.currentMilestoneIndex] : null;
const datum = document.getElementById('milestoneEditDate').value;
const erreicht = document.getElementById('milestoneEditErreicht').checked;
const heute = heuteIso();
const ueber = !!datum && !erreicht && datum < heute;
el.innerHTML = lageBildHtml({
id: m ? 'm_' + m.id : null, start: datum, end: datum, ms: true, fertig: erreicht, ueber,
deps: Array.from(document.querySelectorAll('#milestoneEditDependsOn input[type="checkbox"]:checked')).map(cb => cb.value)
});
const kw = document.getElementById('milestoneEditKw');
const d = parseDateISO(datum);
if(kw){ kw.hidden = !d; kw.textContent = d ? 'KW ' + String(getWeekNumber(d)).padStart(2, '0') : ''; }
document.querySelectorAll('#milestoneEditStand button').forEach(b =>
b.setAttribute('aria-pressed', String((b.dataset.stand === 'erreicht') === erreicht)));
const warn = document.getElementById('milestoneEditWarnung');
if(warn){
const tage = ueber ? daysBetween(parseDateISO(datum), parseDateISO(heute)) : 0;
warn.hidden = !ueber;
warn.textContent = ueber ? 'seit ' + (tage === 1 ? '1 Tag' : tage + ' Tagen') + ' überfällig' : '';
}
};
// msStandSetzen: Umschalter "Offen | Erreicht" — schreibt das (versteckte)
// Feld #milestoneEditErreicht, das saveMilestoneEdit() liest.
window.msStandSetzen = function(erreicht){
document.getElementById('milestoneEditErreicht').checked = !!erreicht;
msLageBildNeu();
};
// applyTodoStatus: schreibt den Status in das Segment und blendet den
// Fortschrittsregler nur im Zustand "In Arbeit" ein (offen = 0 %,
// erledigt = 100 % sind implizit). Parameter: status (String),
@@ -296,6 +438,7 @@
slider.value = value;
if(label) label.textContent = value + ' %';
}
todoLageNeu();
}
// setTodoEditStatus: Klick-Handler des Status-Segments im TODO-Modal.
@@ -313,6 +456,7 @@
todoProgressMemo = value;
const label = document.getElementById('todoEditProgressVal');
if(label) label.textContent = value + ' %';
todoLageNeu();
};
// currentTodoEditStatus: liest den aktuell gewählten Segment-Status aus dem
@@ -335,6 +479,7 @@
badge.hidden = days <= 0;
badge.textContent = days > 0 ? (days + ' AT') : '';
clearFieldError('todoEditRange');
todoLageNeu();
};
// setFieldError / clearFieldError: zeigen einen Validierungshinweis direkt
@@ -401,6 +546,17 @@
const count = list.querySelectorAll('input[type="checkbox"]:checked').length;
badge.textContent = count;
badge.classList.toggle('zero', count === 0);
const summary = document.getElementById('todoEditDependsSummary');
if(summary){
const plan = app.ensurePlanLocal();
summary.innerHTML = Array.from(list.querySelectorAll('input[type="checkbox"]:checked'))
.map(cb => planKnoten(plan, cb.value)).filter(Boolean)
.map(k => `<div class="pd-vorg-zeile"><span class="pd-vorg-stand${k.fertig ? ' is-fertig' : ''}${k.ms ? ' is-ms' : ''}" aria-hidden="true"></span>`
+ `<span class="pd-vorg-titel">${esc(k.titel)}</span>`
+ `<span class="pd-vorg-datum">${k.end ? 'bis ' + esc(kurzDatum(k.end)) : 'ohne Termin'}</span></div>`).join('');
summary.hidden = !summary.innerHTML;
}
todoLageNeu();
};
// filterTodoDepends: blendet in der Vorgänger-Liste alle Einträge aus, die
@@ -459,6 +615,8 @@
const neu = document.getElementById('todoEditSaveNew');
if(abs) abs.hidden = !an;
if(wer) wer.hidden = an;
const aufwandAbs = document.getElementById('todoEditAufwandAbs');
if(aufwandAbs) aufwandAbs.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');
@@ -644,6 +802,7 @@
}
todoAufwandInfo();
todoPhasenModusSetzen(isNew ? !!pre.phase : !!currentTodo.lph, isNew ? null : currentTodo, isNew, isNew ? pre.lph : null);
panelWoSetzen('todoModalWo', currentTodo && typeof aufgabenPhase === 'function' ? aufgabenPhase(currentTodo, plan.todos) : '');
const storedProgress = isNew ? 0 : (parseInt(currentTodo.progress, 10) || 0);
todoProgressMemo = (storedProgress > 0 && storedProgress < 100) ? storedProgress : 50;
@@ -687,9 +846,10 @@
const searchInput = document.getElementById('todoEditDependsSearch');
if(searchInput) searchInput.value = '';
refreshTodoDependsCount();
// Bereich nur aufklappen, wenn es dort auch etwas zu sehen gibt
// Gewählte Vorgänger stehen offen darüber (#todoEditDependsSummary), die
// Auswahlliste bleibt zu, bis man sie ändern will.
const fold = document.getElementById('todoEditDependsFold');
if(fold) fold.open = !!(currentTodo && Array.isArray(currentTodo.dependsOn) && currentTodo.dependsOn.length > 0);
if(fold) fold.open = false;
const assigneesDiv = document.getElementById('todoEditAssignees');
if(assigneesDiv){
@@ -761,6 +921,7 @@
}
document.getElementById('todoModal').style.display = 'none';
app.currentTodoIndex = null;
if(currentTab === 'wochenbericht') berichtAufgabenNeu();
todoModalSnapshot = '';
const danach = todoNachSchliessen;
todoNachSchliessen = null;
+3 -4
View File
@@ -107,9 +107,8 @@
if(nav) nav.style.display = '';
}
// upcomingEntries: offene Aufgaben mit Endtermin und Meilensteine ab heute
// (vergangene Meilensteine haben kein Erledigt-Kennzeichen und gelten als
// gewesen), chronologisch. idx verweist in plan.todos/plan.milestones, damit
// upcomingEntries: offene Aufgaben mit Endtermin und nicht erreichte
// Meilensteine (vergangene gelten als überfällig), chronologisch. idx verweist in plan.todos/plan.milestones, damit
// ein Klick denselben Dialog öffnet wie im Terminplan.
// Rückgabe: Array von { art, idx, titel, datum, iso, wer, meins }.
function upcomingEntries(){
@@ -126,7 +125,7 @@
meins: !!ich && (t.assignees || []).includes(ich) });
});
(plan.milestones || []).forEach((m, idx) => {
if(!m.date || m.date < heute) return;
if(!m.date || m.erreicht) return;
rows.push({ art: 'meilenstein', idx, titel: m.name || 'Meilenstein', iso: m.date, wer: [], meins: false });
});
rows.sort((a, b) => a.iso.localeCompare(b.iso));
+3 -3
View File
@@ -166,16 +166,16 @@
const d = parseDateISO(m.date);
const today = new Date();
today.setHours(0, 0, 0, 0);
const overdue = d && d < today;
const overdue = d && d < today && !m.erreicht;
const msAction = locked ? '' : ` onclick="openMilestoneModal(${idx})" title="Zum Bearbeiten klicken"`;
return `<div class="plan-task-row${locked ? ' is-locked' : ''}"${msAction}>
<span class="plan-task-dot"></span>
<span class="plan-task-title">${esc(m.name || 'Meilenstein')}</span>
<span class="plan-task-meta${overdue ? ' overdue' : ''}">${esc(d ? formatDateDE(d) : 'ohne Datum')}</span>
<span class="plan-task-meta${overdue ? ' overdue' : ''}">${m.erreicht ? 'erreicht · ' : ''}${esc(d ? formatDateDE(d) : 'ohne Datum')}</span>
</div>`;
}).join('');
}
const msCount = document.getElementById('planMilestoneCount');
if(msCount) msCount.textContent = milestones.length === 0 ? '' : `${milestones.length} gesamt`;
if(msCount) msCount.textContent = milestones.length === 0 ? '' : `${milestones.filter(m => m.erreicht).length} von ${milestones.length} erreicht`;
};
})();
+1 -1
View File
@@ -822,7 +822,7 @@ function wtTageDaten(w){
// (offen/überfällig) sind über die Kennzahlen oben erreichbar, die heute
// Abwesenden über die Kopfleiste.
(plan.milestones || []).forEach(m => {
if(m.date && m.date >= heute && spalteFuer[m.date]) spalteFuer[m.date].eintraege.push({ art: 'ms', titel: m.name || 'Meilenstein', projekt: hinweisFuer(m.date) + projekt, pid: pr.id, abschnitt: 'terminplan', sort: m.date });
if(m.date && !m.erreicht && m.date >= heute && spalteFuer[m.date]) spalteFuer[m.date].eintraege.push({ art: 'ms', titel: m.name || 'Meilenstein', projekt: hinweisFuer(m.date) + projekt, pid: pr.id, abschnitt: 'terminplan', sort: m.date });
});
});
tage.forEach(t => t.eintraege.sort((a, b) => String(a.sort).localeCompare(String(b.sort))));
+130
View File
@@ -666,12 +666,14 @@ function renderBerichtListe(){
const gruppenkopf = gruppe !== gruppeVorher ? `<div class="bericht-rail-gruppe">${esc(gruppe)}</div>` : '';
gruppeVorher = gruppe;
const name = (p.nr ? p.nr + ' · ' : '') + p.name;
const ueber = berichtAufgabenDaten(p, w, prevWeek).ueberfaellig.length;
return `${gruppenkopf}<div class="bericht-rail-item${istAktiv ? ' is-aktiv' : ''}${kommentarVon(p, wochenberichtGewerk).besprochen ? ' is-ab' : ''}"
role="button" tabindex="0" data-uid="${esc(p.projectUid || '')}"
aria-current="${istAktiv ? 'true' : 'false'}">
<span class="bericht-rail-num">${String(position).padStart(2, '0')}</span>
${info.bucket === 'unchanged' ? '' : `<span class="bericht-rail-dot is-${esc(info.bucket)}" aria-hidden="true"></span>`}
<span class="bericht-rail-name">${esc(name)}</span>
${ueber ? `<span class="bericht-rail-ueber" title="${ueber} überfällige Aufgabe${ueber === 1 ? '' : 'n'}">${ueber}</span>` : ''}
</div>`;
}).join('');
@@ -713,6 +715,7 @@ function renderBerichtDetail(p, w, prevWeek, locked){
<div class="bericht-sub">${subText}</div>
${fehlen.length ? `<span class="bericht-abwesend-hinweis">Heute abwesend: ${esc(fehlen.join(', '))} — eventuell vertagen.</span>` : ''}
${renderBoardDrawer(p, allPeople, kLocked, w.label, Object.assign({}, BERICHT_OPTS, { ohneVerlauf: true }))}
${renderBerichtAufgaben(p, w, prevWeek, locked)}
<div class="bericht-detail-fuss">
${!locked && hatRecht('proj.plan') ? `<button type="button" class="secondary small" onclick="openTodoAusKommentar('${esc(p.id)}')">
Aufgabe aus Kommentar…
@@ -876,6 +879,133 @@ function berichtVerlaufNeu(){
el.outerHTML = renderBerichtVerlauf(p, w, isWeekLocked(w));
}
// ---------- Aufgaben des Projekts (Eriks Wahl „B“) ----------
//
// Die drei Fragen der Runde: was ist seit der Vorwoche erledigt, was ist
// überfällig, was steht in den nächsten zwei Wochen an. Nur Anzeige — ein
// Klick öffnet dasselbe Aufgaben-/Meilenstein-Panel wie im Terminplan
// ("Ein Ort je Angabe"). Keine neue Datenhaltung: "erledigt seit" ist der
// Vergleich mit der Kopie des Projekts in der vorigen geladenen Sitzung.
// Phasenbalken (t.lph) zählen nicht, sie stehen als Leistungsphasen in der Karte.
const BERICHT_AUFGABEN_TAGE = 14;
const BERICHT_AUFGABEN_JE_SPALTE = 6;
// berichtAufgabenDaten: Parameter p, w, prevWeek. Rückgabe:
// {erledigt, ueberfaellig, bald, weitere, mitVorwoche} — Listen von
// {art, idx, titel, datum, progress, neu, kuerzel}. Aufgaben mit Zuständigen
// aus dem gewählten Gewerk stehen in jeder Liste zuerst. Auch von js/pdf.js genutzt.
function berichtAufgabenDaten(p, w, prevWeek){
const plan = p.plan || {};
const heute = heuteIso();
const bis = isoPlusTage(BERICHT_AUFGABEN_TAGE);
const vorher = prevWeek && p.projectUid ? (prevWeek.projects || []).find(x => x.projectUid === p.projectUid) : null;
const vorherTodos = new Map(((vorher && vorher.plan && vorher.plan.todos) || []).map(t => [String(t.id), t]));
const leute = new Map((w.people || []).map(person => [person.id, person]));
const g = wochenberichtGewerk;
const erledigt = [], ueberfaellig = [], bald = [];
let weitere = 0;
(plan.todos || []).forEach((t, idx) => {
if(t.lph) return;
const wer = (t.assignees || []).map(id => leute.get(id)).filter(Boolean);
const alt = vorherTodos.get(String(t.id));
const e = {
art: 'aufgabe', idx, titel: t.title || 'Aufgabe', datum: t.end || '',
progress: parseInt(t.progress, 10) || 0, fertig: !!t.done,
neu: !!vorher && !alt,
kuerzel: wer.map(person => person.name).join(', '),
imGewerk: !!g && wer.some(person => personGewerk(person) === g)
};
if(t.done){
if(vorher && (!alt || !alt.done)) erledigt.push(e);
} else if(t.end && t.end < heute) ueberfaellig.push(e);
else if(t.end && t.end <= bis) bald.push(e);
else weitere++;
});
(plan.milestones || []).forEach((m, idx) => {
if(!m.date || m.erreicht) return;
const e = { art: 'meilenstein', idx, titel: m.name || 'Meilenstein', datum: m.date, progress: 0, fertig: false, neu: false, kuerzel: '', imGewerk: false };
if(m.date < heute) ueberfaellig.push(e);
else if(m.date <= bis) bald.push(e);
});
const sortieren = l => l.sort((a, b) => (b.imGewerk - a.imGewerk) || String(a.datum).localeCompare(String(b.datum)));
return { erledigt: sortieren(erledigt), ueberfaellig: sortieren(ueberfaellig), bald: sortieren(bald), weitere, mitVorwoche: !!vorher };
}
// renderBerichtAufgaben: der Abschnitt unter der Projektkarte.
// Parameter: p, w, prevWeek, locked. Rückgabe: HTML.
function renderBerichtAufgaben(p, w, prevWeek, locked){
const d = berichtAufgabenDaten(p, w, prevWeek);
const klickbar = !locked && hatRecht('proj.plan');
const vorKw = prevWeek ? berichtKurzKw(prevWeek.label) : '';
const zeile = e => {
const datum = e.datum ? formatDateDE(parseDateISO(e.datum)).slice(0, 6) : '';
const art = e.art === 'meilenstein'
? '<span class="ba-raute" aria-label="Meilenstein"></span>'
: `<span class="ba-haken${e.fertig ? ' is-an' : ''}" aria-hidden="true"></span>`;
const inhalt = `${art}
<span class="ba-titel">${esc(e.titel)}${e.neu ? '<span class="ba-neu">neu</span>' : ''}${!e.fertig && e.progress > 0 && e.progress < 100 ? `<span class="ba-fort">${e.progress} %</span>` : ''}</span>
<span class="ba-wer">${esc(e.kuerzel)}</span>
<span class="ba-datum">${esc(datum)}</span>`;
return klickbar
? `<button type="button" class="ba-zeile" onclick="berichtEintragOeffnen('${esc(p.id)}','${e.art}',${e.idx})">${inhalt}</button>`
: `<div class="ba-zeile">${inhalt}</div>`;
};
const spalte = (titel, liste, leer, klasse) => `<div class="ba-spalte">
<div class="ba-spalte-kopf${klasse || ''}"><span>${esc(titel)}</span><span class="ba-zahl">${liste.length}</span></div>
${liste.length ? liste.slice(0, BERICHT_AUFGABEN_JE_SPALTE).map(zeile).join('') : `<div class="ba-leer">${esc(leer)}</div>`}
${liste.length > BERICHT_AUFGABEN_JE_SPALTE ? `<div class="ba-leer">${liste.length - BERICHT_AUFGABEN_JE_SPALTE} weitere</div>` : ''}
</div>`;
const hinweis = [];
if(d.weitere) hinweis.push(d.weitere === 1 ? '1 weitere offene Aufgabe später oder ohne Termin' : d.weitere + ' weitere offene Aufgaben später oder ohne Termin');
if(klickbar) hinweis.push('Klick auf eine Zeile öffnet die Aufgabe');
return `<section class="bericht-aufgaben" id="berichtAufgaben" aria-label="Aufgaben">
<div class="bv-kopf">
<h2 class="bv-titel">Aufgaben</h2>
<span class="bv-anzahl">${vorKw ? 'seit ' + esc(vorKw) : 'keine Vorwoche geladen'}${wochenberichtGewerk ? ' · Zuständige aus ' + esc(wochenberichtGewerk) + ' zuerst' : ''}</span>
<span class="bericht-fuss-spacer"></span>
<button type="button" class="secondary small" onclick="berichtAufgabenImProjekt('${esc(p.id)}')">Alle im Projekt</button>
</div>
<div class="ba-spalten">
${spalte(vorKw ? 'Erledigt seit ' + vorKw : 'Erledigt', d.erledigt, d.mitVorwoche ? 'Nichts erledigt' : 'Kein Vergleich möglich')}
${spalte('Überfällig', d.ueberfaellig, 'Nichts überfällig', d.ueberfaellig.length ? ' is-rot' : '')}
${spalte('Nächste zwei Wochen', d.bald, 'Nichts fällig')}
</div>
${hinweis.length ? `<div class="ba-hinweis">${esc(hinweis.join(' · '))}</div>` : ''}
</section>`;
}
// berichtAufgabenNeu: zeichnet nur diesen Abschnitt und die Tagesordnung neu
// (nach dem Schließen des Panels), ein angefangener Kommentar bleibt.
function berichtAufgabenNeu(){
const w = currentWeek();
const p = w && aktuellesBerichtProjekt(w);
const el = document.getElementById('berichtAufgaben');
if(!p || !el) return;
el.outerHTML = renderBerichtAufgaben(p, w, findPreviousWeek(w), isWeekLocked(w));
renderBerichtListe();
}
// berichtEintragOeffnen: öffnet das Aufgaben- bzw. Meilenstein-Panel über dem
// Wochenbericht. Die Panels arbeiten auf window.PlanApp; showTab('plan')
// setzt week/project über openPlanView() ohnehin neu.
function berichtEintragOeffnen(pid, art, idx){
const w = currentWeek();
const p = w && (w.projects || []).find(x => x.id === pid);
if(!p) return;
window.PlanApp.week = w;
window.PlanApp.project = p;
if(art === 'meilenstein') openMilestoneModal(idx);
else openTodoModal(idx);
}
// berichtAufgabenImProjekt: springt in den Bereich "Aufgaben" des Projekts.
function berichtAufgabenImProjekt(pid){
location.hash = 'aufgaben';
openProjectPlan(pid);
}
// berichtVerlaufUebernehmen: schreibt den Kommentar jener Sitzung in das
// Kommentarfeld der aktuellen (takeCommentFromHistory(), js/board/board.js).
function berichtVerlaufUebernehmen(weekLabel){
+118
View File
@@ -1872,3 +1872,121 @@ button.notizen-link, button.notizen-link:hover { all: unset; cursor: pointer; co
.plan-nav-item { height: 32px; padding: 0 12px; border-bottom: 0; border-radius: 8px; font-weight: 400; display: inline-flex; align-items: center; }
.plan-nav-item:hover { background: var(--surface-sunk); color: var(--text); }
.plan-nav-item.is-active { background: var(--surface-sunk); color: var(--text); font-weight: 500; border-bottom-color: transparent; box-shadow: inset 0 0 0 1px var(--line-soft); }
/* ===========================================================================
Planblatt — Seitenpanels Aufgabe und Meilenstein (Eriks Wahl, unslop,
11/2026). Kopf: Art, Projekt, Stand und die Lage im Terminplan
(lageBildHtml() in plan-modals.js); darunter die Angaben als Zeilen mit
Beschriftung links, wie im Schriftkopf eines Plans. Ersetzt die
Abschnitte .pd-abs. Werte im 4er-Raster, neutraler Schatten.
=========================================================================== */
.modal-bg.als-panel > .modal.pd { box-shadow: -12px 0 32px rgba(16,20,24,.14); }
@media (prefers-color-scheme: dark) { html:not([data-theme="light"]) .modal-bg.als-panel > .modal.pd { box-shadow: -12px 0 32px rgba(0,0,0,.5); } }
html[data-theme="dark"] .modal-bg.als-panel > .modal.pd { box-shadow: -12px 0 32px rgba(0,0,0,.5); }
.pd-kopf { padding: 16px 20px 0; gap: 12px; border-bottom: 0; }
.pd-art { gap: 8px; height: 32px; font-size: 12px; font-weight: 500; min-width: 0; }
.pd-art > span { flex: none; }
.pd-art .pd-wo { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 400; color: var(--text-subtle); }
.pd-wo:not(:empty)::before { content: "· "; }
.pd-nr { font-family: var(--font-mono); font-size: 11px; }
.pd-art .modal-kbd { margin-left: 0; display: inline-flex; gap: 2px; align-items: center; }
.pd-kasten { width: 10px; height: 10px; border-radius: 3px; }
.pd-raute { width: 9px; height: 9px; }
kbd { font-family: var(--font-mono); font-size: 11px; font-weight: 400; color: var(--text-subtle); border: 1px solid var(--line);
border-bottom-width: 2px; border-radius: 4px; padding: 0 4px; line-height: 16px; background: var(--surface-alt); }
.pd-zu { width: 32px; height: 32px; border-radius: 8px; display: grid; place-items: center; }
.pd-zu svg { width: 16px; height: 16px; }
/* Titel als Überschrift, Rahmen nur beim Zeigen und im Fokus */
.modal-bg.als-panel .pd-kopf .pd-titel,
html.glas .modal-bg.als-panel .pd-kopf .pd-titel {
font-size: 19px; font-weight: 500; line-height: 28px; height: auto; padding: 2px 8px; margin: 0 -8px; width: calc(100% + 16px);
border: 1px solid transparent; border-radius: 8px; background: transparent; box-shadow: none;
}
.modal-bg.als-panel .pd-kopf .pd-titel:hover { border-color: var(--line-soft); }
.modal-bg.als-panel .pd-kopf .pd-titel:focus { border-color: var(--line); outline: none; background: var(--surface); }
.modal-bg.als-panel .pd-kopf .pd-titel.is-error { border-color: var(--red); }
/* Stand direkt unter dem Titel */
.pd-stand { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; min-height: 32px; }
.pd-stand .pd-seg { width: auto; flex: none; }
.pd-stand .pd-seg button { flex: none; min-height: 28px; padding: 0 12px; font-size: 12px; }
.pd-stand .progress-row { flex: 1; min-width: 140px; margin: 0; }
.pd-stand .progress-row output { min-width: 32px; font-family: var(--font-mono); }
#milestoneEditStand button[data-stand="offen"][aria-pressed="true"] .segmented-dot { background: var(--meilenstein); }
#milestoneEditStand button[data-stand="erreicht"][aria-pressed="true"] .segmented-dot { background: var(--green); }
.pd-warnung { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--red); }
.pd-warnung::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--red); }
.pd-warnung[hidden] { display: none; }
/* Lage im Terminplan: sechs Kalenderwochen um den Eintrag */
.pd-lage { margin: 0 -20px; padding: 12px 20px; background: var(--surface-alt); border-block: 1px solid var(--line-soft); display: grid; gap: 4px; }
.pd-lage-leer { font-size: 12px; color: var(--text-subtle); line-height: 20px; }
.pd-lage-skala { position: relative; height: 16px; }
.pd-lage-skala span { position: absolute; top: 0; padding-left: 4px; font-family: var(--font-mono); font-size: 11px; line-height: 16px; color: var(--text-subtle); }
.pd-lage-flaeche { position: relative; height: 52px; overflow: hidden;
background-image: repeating-linear-gradient(to right, var(--line-soft) 0 1px, transparent 1px calc(100% / 6)); }
.pd-lage-bar { position: absolute; height: 12px; border-radius: 3px; overflow: hidden; }
.pd-lage-bar.is-vor { background: color-mix(in srgb, var(--accent) 30%, var(--surface-alt)); }
.pd-lage-bar.is-fertig { background: var(--line); }
.pd-lage-bar.is-nach { background: color-mix(in srgb, var(--accent) 30%, var(--surface-alt)); }
.pd-lage-bar.is-diese { background: color-mix(in srgb, var(--accent) 24%, var(--surface)); box-shadow: inset 0 0 0 1px var(--accent); }
.pd-lage-bar.is-diese.is-fertig { background: color-mix(in srgb, var(--green) 30%, var(--surface)); box-shadow: inset 0 0 0 1px var(--green); }
.pd-lage-bar b { position: absolute; inset: 0 auto 0 0; background: var(--accent); }
.pd-lage-bar.is-fertig b { background: var(--green); }
.pd-lage-raute { position: absolute; width: 10px; height: 10px; margin-top: 1px; background: var(--meilenstein); transform: translateX(-50%) rotate(45deg); }
.pd-lage-raute.is-fertig { background: var(--text-subtle); }
.pd-lage-raute.is-diese { width: 12px; height: 12px; box-shadow: 0 0 0 2px var(--surface-alt), 0 0 0 3px var(--meilenstein); }
.pd-lage-raute.is-diese.is-fertig { background: var(--green); box-shadow: 0 0 0 2px var(--surface-alt), 0 0 0 3px var(--green); }
.pd-lage-raute.is-diese.is-ueber { background: var(--red); box-shadow: 0 0 0 2px var(--surface-alt), 0 0 0 3px var(--red); }
.pd-lage-heute { position: absolute; top: 0; bottom: 0; border-left: 1px dashed var(--text-subtle); }
.pd-lage-legende { display: flex; flex-wrap: wrap; gap: 4px 12px; font-size: 11px; color: var(--text-subtle); }
.pd-lage-legende span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
/* Angaben als Zeilen */
.pd-inhalt { padding: 4px 20px 16px; }
.pd-zeile { display: grid; grid-template-columns: 96px minmax(0, 1fr); gap: 12px; padding: 12px 0; border-bottom: 1px solid var(--line-soft); align-items: start; }
.pd-zeile:last-child { border-bottom: 0; }
.pd-zeile[hidden] { display: none; }
.pd-lab, .pd-lab label { font-size: 12px; font-weight: 400; color: var(--text-subtle); line-height: 32px; }
.pd-wert { display: grid; gap: 8px; min-width: 0; }
.pd-wert .range-picker-display, .pd-wert select { width: 100%; box-sizing: border-box; }
.pd-wert .field-inline { display: flex; align-items: center; gap: 8px; }
.pd-wert .field-inline .range-picker-display { flex: 1; min-width: 0; }
.pd-wert .field-badge { font-family: var(--font-mono); font-weight: 400; }
.pd-wert .pd-hint { font-size: 12px; }
.pd-haken { min-height: 24px; }
@media (max-width: 480px) {
.pd-zeile { grid-template-columns: minmax(0, 1fr); gap: 4px; }
.pd-lab, .pd-lab label { line-height: 20px; }
}
/* Aufwand: Kopf entfällt (Beschriftung steht links), Werte im Raster */
.pd-wert.pd-aufwand { margin: 0; padding: 0; border: 0; gap: 8px; }
.pd-aufwand-zeile { gap: 8px; }
.pd-stepper { height: 32px; }
.pd-stepper:focus-within { border-color: var(--accent); box-shadow: none; }
.pd-aufwand-schnell { gap: 4px; }
.pd-aufwand-schnell button { padding: 0 8px; height: 24px; line-height: 24px; border-radius: 6px; border-color: transparent; }
.pd-aufwand-schnell button[aria-pressed="true"] { font-weight: 500; border-color: transparent; }
.pd-externe { margin-top: 4px; padding-top: 8px; }
/* Vorgänger: gewählte offen, Auswahl eingeklappt */
.pd-vorg { display: grid; gap: 2px; }
.pd-vorg[hidden], .pd-vorg:empty { display: none; }
.pd-vorg-zeile { display: grid; grid-template-columns: 12px minmax(0, 1fr) auto; gap: 8px; align-items: center; height: 32px; padding: 0 8px; border-radius: 6px; background: var(--surface-alt); font-size: 13px; }
.pd-vorg-titel { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pd-vorg-datum { font-family: var(--font-mono); font-size: 11px; color: var(--text-subtle); }
.pd-vorg-stand { width: 10px; height: 10px; border-radius: 3px; border: 1px solid var(--line); }
.pd-vorg-stand.is-fertig { background: var(--green); border-color: var(--green); }
.pd-vorg-stand.is-ms { border-radius: 0; transform: rotate(45deg) scale(.85); border-color: var(--meilenstein); }
.pd-vorg-stand.is-ms.is-fertig { background: var(--green); border-color: var(--green); }
.pd-wert .fold > summary { min-height: 32px; padding: 0 12px; font-size: 12px; color: var(--text-muted); }
.pd-wert .fold-caret { display: inline-grid; place-items: center; width: 16px; height: 16px; }
.pd-wert .fold-caret svg, .pd-wert .fold-search-icon svg { width: 14px; height: 14px; }
.pd-wert .fold-search-icon { display: inline-grid; place-items: center; color: var(--text-subtle); }
.pd-notiz { min-height: 64px; padding: 8px 12px; font-size: 13px; line-height: 20px; }
.pd-stand .progress-row input[type=range] { min-width: 0; width: auto; }
.pd-fuss { padding: 12px 20px calc(12px + env(safe-area-inset-bottom, 0px)); }
+34
View File
@@ -6489,3 +6489,37 @@ button { transition: background-color var(--dur) var(--ease-out), color var(--du
#headerMenu.side-menu.collapsed .nav-item.nav-unterpunkt::before { display: none; }
#headerMenu.side-menu.collapsed .nav-item.nav-unterpunkt .nav-icon { display: inline-flex; }
}
/* ===========================================================================
AUFGABEN IM WOCHENBERICHT (Eriks Wahl „B“, renderBerichtAufgaben() in
js/wochenbericht/wochenbericht.js): drei Spalten unter der Projektkarte,
Kopf wie „Verlauf der Vorwochen“ (.bv-kopf), rote Zahl in der Tagesordnung.
=========================================================================== */
.bericht-rail-ueber { font-family: var(--font-mono); font-size: 11px; color: var(--red); font-variant-numeric: tabular-nums; flex-shrink: 0; }
.bericht-aufgaben { margin-top: 24px; padding-top: 16px; border-top: 1px solid var(--line-soft); }
.ba-spalten { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; margin-top: 4px; }
@media (max-width: 1100px) { .ba-spalten { grid-template-columns: minmax(0, 1fr); } }
.ba-spalte { display: grid; gap: 2px; align-content: start; min-width: 0; }
.ba-spalte-kopf { display: flex; justify-content: space-between; gap: 8px; font-size: 12px; color: var(--text-subtle);
padding: 0 8px 4px; border-bottom: 1px solid var(--line-soft); margin-bottom: 4px; }
.ba-zahl { font-family: var(--font-mono); font-variant-numeric: tabular-nums; color: var(--text-muted); }
.ba-spalte-kopf.is-rot .ba-zahl { color: var(--red); }
.ba-zeile { display: grid; grid-template-columns: 16px minmax(0, 1fr) auto 48px; gap: 8px; align-items: center;
min-height: 32px; padding: 0 8px; border-radius: 6px; font: inherit; font-size: 13px; color: var(--text);
background: none; border: 0; box-shadow: none; text-align: left; width: 100%; }
button.ba-zeile { cursor: pointer; transition: background-color var(--dur) var(--ease-out); }
button.ba-zeile:hover { background: var(--surface-sunk); }
button.ba-zeile:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.ba-titel { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ba-wer { font-family: var(--font-mono); font-size: 11px; color: var(--text-muted); white-space: nowrap; }
.ba-datum { font-family: var(--font-mono); font-size: 11px; color: var(--text-subtle); text-align: right; font-variant-numeric: tabular-nums; }
.ba-spalte-kopf.is-rot ~ .ba-zeile .ba-datum { color: var(--red); }
.ba-neu { font-size: 11px; color: var(--amber); margin-left: 6px; }
.ba-fort { font-family: var(--font-mono); font-size: 11px; color: var(--text-subtle); margin-left: 6px; }
.ba-haken { width: 14px; height: 14px; border-radius: 4px; border: 1px solid var(--line); display: grid; place-items: center; }
.ba-haken.is-an { background: var(--green); border-color: var(--green); }
.ba-haken.is-an::after { content: ""; width: 6px; height: 3px; border: solid var(--bg); border-width: 0 0 1.5px 1.5px; transform: translate(0, -1px) rotate(-45deg); }
.ba-haken.is-an ~ .ba-titel { color: var(--text-subtle); text-decoration: line-through; text-decoration-color: var(--line); }
.ba-raute { width: 9px; height: 9px; margin-left: 2px; background: var(--meilenstein); transform: rotate(45deg); }
.ba-leer, .ba-hinweis { font-size: 12px; color: var(--text-subtle); padding: 4px 8px; }
.ba-hinweis { padding: 8px 0 0; }