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

+46 -3
View File
@@ -1540,6 +1540,24 @@ step into a project explicit. Everything lives in `public/js/auftrag.js`:
Teams-Nachricht. Wer einer Aufgabe zugewiesen ist, aber (nicht mehr) im Teams-Nachricht. Wer einer Aufgabe zugewiesen ist, aber (nicht mehr) im
Projekt, erscheint beim Öffnen automatisch dort. Keine Daten­änderung Projekt, erscheint beim Öffnen automatisch dort. Keine Daten­änderung
am Projekt: die Person wird NICHT unter „Beteiligte“ eingetragen. am Projekt: die Person wird NICHT unter „Beteiligte“ eingetragen.
- **Panels als „Planblatt“** (Eriks Wahl, unslop, 11/2026; löst den
Aufbau mit `.pd-abs`-Abschnitten im folgenden Absatz ab): Kopf mit Art,
Projekt (`#todoModalWo`/`#milestoneModalWo`, `panelWoSetzen()`), Titel
ohne Kasten und dem **Stand direkt darunter** (Aufgabe: Status-Segment +
Fortschritt; Meilenstein: „Offen | Erreicht“ über `msStandSetzen()`, das
versteckte Feld `#milestoneEditErreicht` bleibt die Quelle für
`saveMilestoneEdit()`, dazu „seit N Tagen überfällig“). Darunter die
**Lage im Terminplan** (`lageBildHtml()`, `todoLageNeu()`,
`msLageBildNeu()` in `plan-modals.js`): sechs Kalenderwochen ab einer
Woche vor dem Eintrag (früher, wenn ein Vorgänger davor endet), oben
Vorgänger, Mitte der Eintrag mit Fortschritt, unten Nachfolger,
Heute-Linie; nur Anzeige, neu gezeichnet bei Zeitraum, Stand und
Vorgängern. Dann die Angaben als Zeilen (`.pd-zeile`, Beschriftung
links): Zeitraum, Zuständig, Aufwand (`#todoEditAufwandAbs`, im
Phasen-Modus mit ausgeblendet), Hängt ab von (gewählte Vorgänger offen
in `#todoEditDependsSummary`, Auswahl eingeklappt unter „Vorgänger
ändern“), Notiz. Alle Feld-ids und Schreibwege unverändert. Stile:
Block „Planblatt“ am Ende von `plan-styles.css`.
- **Aufgabe und Meilenstein als Seitenpanel** (Eriks Wahl „B“): - **Aufgabe und Meilenstein als Seitenpanel** (Eriks Wahl „B“):
`#todoModal` und `#milestoneModal` sind `.modal-bg.als-panel` — rechts am `#todoModal` und `#milestoneModal` sind `.modal-bg.als-panel` — rechts am
Rand, volle Höhe, Hintergrund nur leicht abgedunkelt, damit der Rand, volle Höhe, Hintergrund nur leicht abgedunkelt, damit der
@@ -3451,6 +3469,26 @@ the system font offline). `z-index: 400`, above `.modal-bg` (300).
one project is ever open, so height is free while width is scarce — beside one project is ever open, so height is free while width is scarce — beside
the list, three columns left the people 137 px. the list, three columns left the people 137 px.
### Wochenbericht — Aufgaben des Projekts (Eriks Wahl „B“, 11/2026)
Unter der Projektkarte, vor „Besprochen & weiter“, der Abschnitt „Aufgaben“
(`renderBerichtAufgaben()`/`berichtAufgabenDaten()` in
`js/wochenbericht/wochenbericht.js`) mit drei Spalten: **Erledigt seit KW …**
(jetzt `done`, in der vorigen GELADENEN Sitzung offen oder nicht vorhanden —
Abgleich über die Aufgaben-id, ohne Vorwoche leer), **Überfällig** (offen,
Ende vor heute), **Nächste zwei Wochen** (offene Aufgaben und Meilensteine
bis heute + 14). Phasenbalken (`t.lph`) zählen nicht. Aufgaben mit
Zuständigen aus dem gewählten Gewerk stehen zuerst, gezeigt werden aber alle.
Nur Anzeige: ein Klick öffnet das normale Aufgaben-/Meilenstein-Panel ÜBER
dem Wochenbericht (`berichtEintragOeffnen()` setzt dafür
`PlanApp.week/project`; `showTab('plan')` setzt beides ohnehin neu);
`closeTodoModal()`/`closeMilestoneModal()` zeichnen danach nur den Abschnitt
und die Tagesordnung neu (`berichtAufgabenNeu()`), ein angefangener Kommentar
bleibt. Die Tagesordnung zeigt je Projekt die Zahl überfälliger Aufgaben rot
(`.bericht-rail-ueber`). Das PDF hat in jeder Akte die Tabelle „Aufgaben“
(`pdfWbAufgaben()`, höchstens fünf je Gruppe). Stile: Block „AUFGABEN IM
WOCHENBERICHT“ am Ende von `styles.css`.
### Wochenbericht — Gewerke-Trennung ### Wochenbericht — Gewerke-Trennung
Since departments stopped partitioning the data (see "Departments were Since departments stopped partitioning the data (see "Departments were
@@ -3763,9 +3801,14 @@ Terminplänen zu beantworten war. Details:
Altersgrenze stehen — eine Frist verfällt nicht dadurch, dass sie lange Altersgrenze stehen — eine Frist verfällt nicht dadurch, dass sie lange
gerissen ist. Ohne die Grenze nach vorn stünden Termine aus einem Jahr in gerissen ist. Ohne die Grenze nach vorn stünden Termine aus einem Jahr in
der Liste und die dringenden gingen darin unter. der Liste und die dringenden gingen darin unter.
- **Meilensteine haben kein Erledigt-Kennzeichen** (`plan_milestones` hat - **Meilensteine haben ein Kennzeichen „Erreicht“** (`m.erreicht`,
keine `done`-Spalte), deshalb zählt bei ihnen allein das Datum; bei Aufgaben `plan_milestones.erreicht`, Migration in `migrateAddPlanTodoNotiz()`, in
gilt zusätzlich `done = 0` und ein gesetztes Enddatum. `IMPORT_OPTIONAL_COLS`; Häkchen im Meilenstein-Panel unter „Wann“). Ein
vergangener, nicht erreichter Meilenstein gilt überall als überfällig
(Band und Projektblöcke auf „Meine Aufgaben“, Projekt-Dashboard,
Wochenbericht); erreichte fallen aus Terminlisten, „Nächster Meilenstein“
und Wandtafel heraus, im Kalender bleiben sie stehen. Bei Aufgaben gilt
`done = 0` und ein gesetztes Enddatum.
- **Die Namen der Zuständigen kommen aus `week_people`**, nicht aus - **Die Namen der Zuständigen kommen aus `week_people`**, nicht aus
`loadGlobalPeople()`: Die globale Liste enthält nur Personen mit `loadGlobalPeople()`: Die globale Liste enthält nur Personen mit
Benutzerkonto, die Teilnehmerliste einer Sitzung dagegen jede Person, die Benutzerkonto, die Teilnehmerliste einer Sitzung dagegen jede Person, die
+144 -107
View File
@@ -38,8 +38,8 @@
<link rel="icon" type="image/png" href="/logo.png"> <link rel="icon" type="image/png" href="/logo.png">
<link rel="alternate icon" href="/logo.ico"> <link rel="alternate icon" href="/logo.ico">
<link rel="apple-touch-icon" href="/logo.png"> <link rel="apple-touch-icon" href="/logo.png">
<link rel="stylesheet" href="/styles.css?v=123"> <link rel="stylesheet" href="/styles.css?v=124">
<link rel="stylesheet" href="/plan-styles.css?v=56"> <link rel="stylesheet" href="/plan-styles.css?v=57">
<!-- Glas-Darstellung (public/glas.css, als letztes Stylesheet): greift nur mit <!-- Glas-Darstellung (public/glas.css, als letztes Stylesheet): greift nur mit
der Klasse html.glas. Gesetzt wird sie hier, vor dem ersten Zeichnen, der Klasse html.glas. Gesetzt wird sie hier, vor dem ersten Zeichnen,
damit die Seite nicht erst ohne Glas aufblitzt; abschaltbar je Browser 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, 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 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. --> 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-bg als-panel" id="milestoneModal" style="display:none">
<div class="modal pd"> <div class="modal pd">
<div class="pd-kopf"> <div class="pd-kopf">
<div class="pd-art"><span class="pd-raute" aria-hidden="true"></span><span id="milestoneModalTitle">Meilenstein</span> <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> <span class="pd-wo" id="milestoneModalWo"></span>
<button type="button" class="pd-zu" onclick="closeMilestoneModal()" aria-label="Schließen">×</button></div> <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"> <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>
<div class="pd-inhalt"> <div class="pd-inhalt">
<section class="pd-abs"> <div class="pd-zeile">
<h3>Wann</h3> <span class="pd-lab">Datum</span>
<!-- sichtbares Anzeigefeld (deutsches Format) + verstecktes ISO-Feld --> <div class="pd-wert">
<input type="text" id="milestoneEditDateDisplay" class="range-picker-display" placeholder="Datum wählen…" readonly aria-label="Datum" <!-- sichtbares Anzeigefeld (deutsches Format) + verstecktes ISO-Feld -->
onclick="openDatePicker({ anchor: this, inputId: 'milestoneEditDate', displayId: 'milestoneEditDateDisplay' })"> <div class="field-inline">
<input type="hidden" id="milestoneEditDate"> <input type="text" id="milestoneEditDateDisplay" class="range-picker-display" placeholder="Datum wählen…" readonly aria-label="Datum"
</section> onclick="openDatePicker({ anchor: this, inputId: 'milestoneEditDate', displayId: 'milestoneEditDateDisplay', onApply: msLageBildNeu })">
<section class="pd-abs"> <span class="field-badge pd-kw" id="milestoneEditKw" hidden></span>
<h3>Wo im Terminplan</h3> </div>
<div class="segmented pd-seg" id="milestoneEditLage"> <input type="hidden" id="milestoneEditDate">
<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> </div>
<select id="milestoneEditPosition" aria-label="Position bei einer Aufgabe" onchange="msLageSync()"> </div>
<option value="">Oben in der Projektzeile</option> <div class="pd-zeile">
</select> <span class="pd-lab">Im Terminplan</span>
</section> <div class="pd-wert">
<section class="pd-abs"> <div class="segmented pd-seg" id="milestoneEditLage">
<h3>Hängt ab von</h3> <button type="button" data-lage="zeile" aria-pressed="false" onclick="msLageSetzen('zeile')">Eigene Zeile oben</button>
<div id="milestoneEditDependsOn" class="todo-depends-list pd-liste"></div> <button type="button" data-lage="aufgabe" aria-pressed="false" onclick="msLageSetzen('aufgabe')">Bei einer Aufgabe</button>
</section> </div>
<section class="pd-abs"> <select id="milestoneEditPosition" aria-label="Position bei einer Aufgabe" onchange="msLageSync()">
<h3>Notiz</h3> <option value="">Oben in der Projektzeile</option>
<p class="hint pd-hint pd-mail-hinweis" id="milestoneEditMailHinweis" hidden></p> </select>
<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> </div>
</section> </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>
<div class="pd-fuss"> <div class="pd-fuss">
<button type="button" class="pd-loeschen" id="milestoneEditDelete" onclick="deleteMilestoneEdit()">Löschen</button> <button type="button" class="pd-loeschen" id="milestoneEditDelete" onclick="deleteMilestoneEdit()">Löschen</button>
@@ -1549,51 +1575,69 @@
<div class="modal pd"> <div class="modal pd">
<div class="pd-kopf"> <div class="pd-kopf">
<div class="pd-art"><span class="pd-kasten" aria-hidden="true"></span><span id="todoModalTitle">Aufgabe bearbeiten</span> <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> <span class="pd-wo" id="todoModalWo"></span>
<button type="button" class="pd-zu" onclick="closeTodoModal()" aria-label="Schließen">×</button></div> <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"> <input type="text" id="todoEditTitle" class="pd-titel" placeholder="Titel der Aufgabe" aria-label="Titel">
<span class="field-error" id="todoEditTitleError" hidden></span> <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>
<div class="pd-inhalt"> <div class="pd-inhalt">
<!-- Nur im Phasen-Modus ("+ Neu" → "Leistungsphase" oder Bearbeiten <!-- Nur im Phasen-Modus ("+ Neu" → "Leistungsphase" oder Bearbeiten
eines Phasenbalkens): Welche Leistungsphase dieser Balken ist. Nur eines Phasenbalkens): Welche Leistungsphase dieser Balken ist. Nur
darüber wird ein Balken zum Phasenkopf, nicht mehr über den Titel. --> darüber wird ein Balken zum Phasenkopf, nicht mehr über den Titel. -->
<section class="pd-abs" id="todoEditPhaseAbs" hidden> <div class="pd-zeile" id="todoEditPhaseAbs" hidden>
<h3>Leistungsphase</h3> <span class="pd-lab"><label for="todoEditLph">Leistungsphase</label></span>
<select id="todoEditLph" aria-label="Leistungsphase" onchange="todoPhaseGewechselt()"></select> <div class="pd-wert">
<p class="hint pd-hint">Aufgaben, die im Zeitraum dieser Phase beginnen, stehen im Terminplan darunter.</p> <select id="todoEditLph" onchange="todoPhaseGewechselt()"></select>
</section> <p class="hint pd-hint">Aufgaben, die im Zeitraum dieser Phase beginnen, stehen im Terminplan darunter.</p>
<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> </div>
<input type="hidden" id="todoEditStart"> </div>
<input type="hidden" id="todoEditEnd"> <div class="pd-zeile">
<span class="field-error" id="todoEditRangeError" hidden></span> <span class="pd-lab">Zeitraum</span>
<label class="pd-haken"><input type="checkbox" id="todoEditShowInGantt" checked> Im Terminplan als Balken zeigen</label> <div class="pd-wert">
<p class="hint pd-hint">Ohne Zeitraum erscheint die Aufgabe nur unter „Aufgaben“, nicht im Terminplan.</p> <div class="field-inline">
</section> <input type="text" id="todoEditRange" class="range-picker-display" placeholder="Start – Ende wählen…" readonly aria-label="Zeitraum"
<section class="pd-abs" id="todoEditWerAbs"> onclick="openRangePicker({ anchor: this, startInputId: 'todoEditStart', endInputId: 'todoEditEnd', displayId: 'todoEditRange', onApply: refreshTodoDuration })">
<h3>Wer</h3> <span class="field-badge" id="todoEditDuration" hidden></span>
<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>
<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-aufwand-zeile">
<div class="pd-stepper"> <div class="pd-stepper">
<button type="button" class="pd-stepper-knopf" onclick="todoAufwandAendern(-1)" aria-label="Eine Stunde weniger">−</button> <button type="button" class="pd-stepper-knopf" onclick="todoAufwandAendern(-1)" aria-label="Eine Stunde weniger">−</button>
@@ -1611,41 +1655,34 @@
</div> </div>
<span class="pd-aufwand-info" id="todoAufwandInfoText"></span> <span class="pd-aufwand-info" id="todoAufwandInfoText"></span>
</div> </div>
</section> </div>
<section class="pd-abs"> <div class="pd-zeile">
<h3>Stand</h3> <span class="pd-lab">Hängt ab von</span>
<div class="segmented pd-seg" id="todoEditStatus"> <div class="pd-wert">
<button type="button" data-status="offen" aria-pressed="false" onclick="setTodoEditStatus('offen')"><span class="segmented-dot"></span>Offen</button> <div class="pd-vorg" id="todoEditDependsSummary"></div>
<button type="button" data-status="arbeit" aria-pressed="false" onclick="setTodoEditStatus('arbeit')"><span class="segmented-dot"></span>In Arbeit</button> <details class="fold" id="todoEditDependsFold">
<button type="button" data-status="fertig" aria-pressed="false" onclick="setTodoEditStatus('fertig')"><span class="segmented-dot"></span>Erledigt</button> <summary>
</div> <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>
<div class="progress-row" id="todoEditProgressRow"> <span>Vorgänger ändern</span>
<input type="range" id="todoEditProgress" min="0" max="100" step="5" value="0" aria-label="Fortschritt" <span class="fold-count zero" id="todoEditDependsCount">0</span>
oninput="onTodoProgressInput(this)"> </summary>
<output id="todoEditProgressVal">0 %</output> <div class="fold-body">
</div> <div class="fold-search">
</section> <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>
<section class="pd-abs"> <input type="text" id="todoEditDependsSearch" placeholder="Aufgabe oder Meilenstein suchen" oninput="filterTodoDepends()">
<h3>Notiz</h3> </div>
<p class="hint pd-hint pd-mail-hinweis" id="todoEditMailHinweis" hidden></p> <div id="todoEditDependsOn" class="todo-depends-list flat" onchange="refreshTodoDependsCount()"></div>
<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>
<div id="todoEditDependsOn" class="todo-depends-list flat" onchange="refreshTodoDependsCount()"></div> </details>
</div> </div>
</details> </div>
</section> <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>
<div class="pd-fuss"> <div class="pd-fuss">
<button type="button" class="pd-loeschen" id="todoEditDelete" onclick="deleteTodoEdit()">Löschen</button> <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/store.js?v=17"></script>
<script src="/js/weeks.js?v=12"></script> <script src="/js/weeks.js?v=12"></script>
<script src="/js/board/board.js?v=42"></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/people/people.js?v=8"></script>
<script src="/js/planung/planung.js?v=3"></script> <script src="/js/planung/planung.js?v=3"></script>
<script src="/js/planchecker/planchecker.js?v=1"></script> <script src="/js/planchecker/planchecker.js?v=1"></script>
@@ -1977,7 +2014,7 @@
<!-- Assistent "Neues Projekt": braucht auftrag.js (Auftragsstatus) und <!-- Assistent "Neues Projekt": braucht auftrag.js (Auftragsstatus) und
load.js (faktorFormat), steht deshalb dahinter. --> load.js (faktorFormat), steht deshalb dahinter. -->
<script src="/js/projekt-wizard.js?v=6"></script> <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> <script src="/js/benachrichtigungen.js?v=5"></script>
<!-- wand.js braucht berechneAuslastung()/loadAmpelColor()/kapazitaetsFaktor()/ <!-- wand.js braucht berechneAuslastung()/loadAmpelColor()/kapazitaetsFaktor()/
stundenFormat() (load.js), classifyProjectChange()/findPreviousWeek() stundenFormat() (load.js), classifyProjectChange()/findPreviousWeek()
@@ -1985,7 +2022,7 @@
relativerTag()/tageBis()/initialsFor() (dashboard.js) — alle laden relativerTag()/tageBis()/initialsFor() (dashboard.js) — alle laden
bereits vorher, die Reihenfolge hier ist also nur Zuordnung zum bereits vorher, die Reihenfolge hier ist also nur Zuordnung zum
Tab-Ordner, keine neue Abhängigkeit nach vorn. --> 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/settings/settings.js?v=14"></script>
<script src="/js/admin/admin.js?v=24"></script> <script src="/js/admin/admin.js?v=24"></script>
<script src="/js/admin-users.js?v=10"></script> <script src="/js/admin-users.js?v=10"></script>
@@ -2000,14 +2037,14 @@
js/gantt.js kommen, dessen buildGanttHtml() plan-build-gantt.js js/gantt.js kommen, dessen buildGanttHtml() plan-build-gantt.js
umhuellt. --> umhuellt. -->
<script src="/js/plan/plan-state.js?v=25"></script> <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-info.js?v=17"></script>
<script src="/js/plan/plan-history.js?v=4"></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-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-drag.js?v=8"></script>
<script src="/js/plan/plan-modals.js?v=18"></script> <script src="/js/plan/plan-modals.js?v=21"></script>
<script src="/js/plan/plan-tasks.js?v=4"></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-phasen.js?v=3"></script>
<script src="/js/plan/plan-files.js?v=4"></script> <script src="/js/plan/plan-files.js?v=4"></script>
<script src="/js/plan/plan-protokoll.js?v=21"></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-settings.js?v=14"></script>
<script src="/js/plan/plan-schedule.js?v=7"></script> <script src="/js/plan/plan-schedule.js?v=7"></script>
<script src="/js/tabs-auth.js?v=19"></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/backup.js?v=2"></script>
<script src="/js/import.js?v=1"></script> <script src="/js/import.js?v=1"></script>
<script src="/js/init.js?v=14"></script> <script src="/js/init.js?v=14"></script>
+5 -5
View File
@@ -212,14 +212,14 @@ function dashDaten(){
}); });
(plan.milestones || []).forEach(m => { (plan.milestones || []).forEach(m => {
if(!m.date) return; 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); const hatEigene = termine.some(t => t.meins);
if(!rollen.length && !hatEigene) return; if(!rollen.length && !hatEigene) return;
// Erledigte Vergangenheit interessiert hier nicht: Meilensteine vor heute // Erreichte Meilensteine fallen weg (im Kalender bleiben sie stehen);
// fallen weg (sie haben kein Erledigt-Kennzeichen), offene Aufgaben vor // nicht erreichte vor heute gelten wie offene Aufgaben als überfällig —
// heute bleiben als überfällig stehen. // dieselbe Regel wie die Zahl im Band (GET /my-dashboard).
const relevant = termine.filter(t => !(t.art === 'meilenstein' && t.datum < heute)); const relevant = termine.filter(t => !(t.art === 'meilenstein' && t.erreicht));
relevant.sort((a, b) => (a.datum || '9999').localeCompare(b.datum || '9999')); relevant.sort((a, b) => (a.datum || '9999').localeCompare(b.datum || '9999'));
projekte.push({ p, rollen, termine: relevant, alleTermine: termine }); 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] }); color: '#D70015', margin: [0, 6, 0, 0] });
} }
const innerContent = { const aufgaben = pdfWbAufgaben(p, w, prevWeek);
const innerContent = { stack: [{
columns: [ columns: [
{ width: '*', stack: [kopf, aenderung, kommentar].concat(vorher ? [vorher] : []) }, { width: '*', stack: [kopf, aenderung, kommentar].concat(vorher ? [vorher] : []) },
{ width: 190, stack: rightStack } { width: 190, stack: rightStack }
], ],
columnGap: 12 columnGap: 12
}; }].concat(aufgaben ? [aufgaben] : []) };
return { return {
table: { 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 // pdfWbVorigerKommentar: Block mit dem jüngsten früheren Kommentar des
// Gewerks. Steht er in der direkten Vorwoche, heißt er "Vorwoche KW …", // Gewerks. Steht er in der direkten Vorwoche, heißt er "Vorwoche KW …",
// sonst "Zuletzt KW …". Parameter: p (Projekt), w (Sitzung). Rückgabe: // 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 openSub = overdue.length > 0 ? overdue.length + ' überfällig' : (open.length > 0 ? 'im Zeitplan' : 'alle erledigt');
const upcoming = milestones 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)); .sort((a, b) => parseDateISO(a.date) - parseDateISO(b.date));
const msZelle = upcoming.length > 0 const msZelle = upcoming.length > 0
? bandZelle('Nächster Meilenstein', formatDateDE(parseDateISO(upcoming[0].date)), upcoming[0].name || 'Meilenstein') ? 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; document.getElementById('milestoneEditDate').value = msDateIso;
const msDateDisplay = document.getElementById('milestoneEditDateDisplay'); const msDateDisplay = document.getElementById('milestoneEditDateDisplay');
if(msDateDisplay) msDateDisplay.value = msDateIso ? formatDateDE(parseDateISO(msDateIso)) : ''; 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'); const positionSelect = document.getElementById('milestoneEditPosition');
if(positionSelect){ if(positionSelect){
@@ -129,6 +132,7 @@
dependsOnList.innerHTML = listHtml; dependsOnList.innerHTML = listHtml;
} }
msLageBildNeu();
document.getElementById('milestoneModal').style.display = 'flex'; document.getElementById('milestoneModal').style.display = 'flex';
document.getElementById('milestoneEditName').focus(); document.getElementById('milestoneEditName').focus();
}; };
@@ -167,6 +171,7 @@
window.closeMilestoneModal = function(){ window.closeMilestoneModal = function(){
document.getElementById('milestoneModal').style.display = 'none'; document.getElementById('milestoneModal').style.display = 'none';
app.currentMilestoneIndex = null; app.currentMilestoneIndex = null;
if(currentTab === 'wochenbericht') berichtAufgabenNeu();
const danach = msNachSchliessen; const danach = msNachSchliessen;
msNachSchliessen = null; msNachSchliessen = null;
if(danach) setTimeout(danach, 0); if(danach) setTimeout(danach, 0);
@@ -189,12 +194,14 @@
const position = positionSelect ? positionSelect.value : ''; const position = positionSelect ? positionSelect.value : '';
const msNotizEl = document.getElementById('milestoneEditNotiz'); const msNotizEl = document.getElementById('milestoneEditNotiz');
const notiz = msNotizEl ? msNotizEl.value.trim() : ''; const notiz = msNotizEl ? msNotizEl.value.trim() : '';
const msErreichtEl = document.getElementById('milestoneEditErreicht');
const erreicht = !!(msErreichtEl && msErreichtEl.checked);
const plan = app.ensurePlanLocal(); const plan = app.ensurePlanLocal();
if(!Array.isArray(plan.milestones)) plan.milestones = []; if(!Array.isArray(plan.milestones)) plan.milestones = [];
if(app.currentMilestoneIndex === null){ 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]){ } else if(plan.milestones[app.currentMilestoneIndex]){
const m = plan.milestones[app.currentMilestoneIndex]; const m = plan.milestones[app.currentMilestoneIndex];
m.name = name; m.name = name;
@@ -202,6 +209,7 @@
m.dependsOn = dependsOn; m.dependsOn = dependsOn;
m.position = position; m.position = position;
m.notiz = notiz; m.notiz = notiz;
m.erreicht = erreicht;
} }
plan.milestones.sort((a,b) => parseDateISO(a.date) - parseDateISO(b.date)); plan.milestones.sort((a,b) => parseDateISO(a.date) - parseDateISO(b.date));
app.savePlan(); app.savePlan();
@@ -266,6 +274,140 @@
return progress > 0 ? 'arbeit' : 'offen'; 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 // applyTodoStatus: schreibt den Status in das Segment und blendet den
// Fortschrittsregler nur im Zustand "In Arbeit" ein (offen = 0 %, // Fortschrittsregler nur im Zustand "In Arbeit" ein (offen = 0 %,
// erledigt = 100 % sind implizit). Parameter: status (String), // erledigt = 100 % sind implizit). Parameter: status (String),
@@ -296,6 +438,7 @@
slider.value = value; slider.value = value;
if(label) label.textContent = value + ' %'; if(label) label.textContent = value + ' %';
} }
todoLageNeu();
} }
// setTodoEditStatus: Klick-Handler des Status-Segments im TODO-Modal. // setTodoEditStatus: Klick-Handler des Status-Segments im TODO-Modal.
@@ -313,6 +456,7 @@
todoProgressMemo = value; todoProgressMemo = value;
const label = document.getElementById('todoEditProgressVal'); const label = document.getElementById('todoEditProgressVal');
if(label) label.textContent = value + ' %'; if(label) label.textContent = value + ' %';
todoLageNeu();
}; };
// currentTodoEditStatus: liest den aktuell gewählten Segment-Status aus dem // currentTodoEditStatus: liest den aktuell gewählten Segment-Status aus dem
@@ -335,6 +479,7 @@
badge.hidden = days <= 0; badge.hidden = days <= 0;
badge.textContent = days > 0 ? (days + ' AT') : ''; badge.textContent = days > 0 ? (days + ' AT') : '';
clearFieldError('todoEditRange'); clearFieldError('todoEditRange');
todoLageNeu();
}; };
// setFieldError / clearFieldError: zeigen einen Validierungshinweis direkt // setFieldError / clearFieldError: zeigen einen Validierungshinweis direkt
@@ -401,6 +546,17 @@
const count = list.querySelectorAll('input[type="checkbox"]:checked').length; const count = list.querySelectorAll('input[type="checkbox"]:checked').length;
badge.textContent = count; badge.textContent = count;
badge.classList.toggle('zero', count === 0); 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 // filterTodoDepends: blendet in der Vorgänger-Liste alle Einträge aus, die
@@ -459,6 +615,8 @@
const neu = document.getElementById('todoEditSaveNew'); const neu = document.getElementById('todoEditSaveNew');
if(abs) abs.hidden = !an; if(abs) abs.hidden = !an;
if(wer) wer.hidden = an; if(wer) wer.hidden = an;
const aufwandAbs = document.getElementById('todoEditAufwandAbs');
if(aufwandAbs) aufwandAbs.hidden = an;
if(neu) neu.hidden = an; if(neu) neu.hidden = an;
const titelEl = document.getElementById('todoModalTitle'); const titelEl = document.getElementById('todoModalTitle');
if(titelEl) titelEl.textContent = an ? (isNew ? 'Neue Leistungsphase' : 'Leistungsphase bearbeiten') : (isNew ? 'Neue Aufgabe' : 'Aufgabe bearbeiten'); if(titelEl) titelEl.textContent = an ? (isNew ? 'Neue Leistungsphase' : 'Leistungsphase bearbeiten') : (isNew ? 'Neue Aufgabe' : 'Aufgabe bearbeiten');
@@ -644,6 +802,7 @@
} }
todoAufwandInfo(); todoAufwandInfo();
todoPhasenModusSetzen(isNew ? !!pre.phase : !!currentTodo.lph, isNew ? null : currentTodo, isNew, isNew ? pre.lph : null); 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); const storedProgress = isNew ? 0 : (parseInt(currentTodo.progress, 10) || 0);
todoProgressMemo = (storedProgress > 0 && storedProgress < 100) ? storedProgress : 50; todoProgressMemo = (storedProgress > 0 && storedProgress < 100) ? storedProgress : 50;
@@ -687,9 +846,10 @@
const searchInput = document.getElementById('todoEditDependsSearch'); const searchInput = document.getElementById('todoEditDependsSearch');
if(searchInput) searchInput.value = ''; if(searchInput) searchInput.value = '';
refreshTodoDependsCount(); 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'); 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'); const assigneesDiv = document.getElementById('todoEditAssignees');
if(assigneesDiv){ if(assigneesDiv){
@@ -761,6 +921,7 @@
} }
document.getElementById('todoModal').style.display = 'none'; document.getElementById('todoModal').style.display = 'none';
app.currentTodoIndex = null; app.currentTodoIndex = null;
if(currentTab === 'wochenbericht') berichtAufgabenNeu();
todoModalSnapshot = ''; todoModalSnapshot = '';
const danach = todoNachSchliessen; const danach = todoNachSchliessen;
todoNachSchliessen = null; todoNachSchliessen = null;
+3 -4
View File
@@ -107,9 +107,8 @@
if(nav) nav.style.display = ''; if(nav) nav.style.display = '';
} }
// upcomingEntries: offene Aufgaben mit Endtermin und Meilensteine ab heute // upcomingEntries: offene Aufgaben mit Endtermin und nicht erreichte
// (vergangene Meilensteine haben kein Erledigt-Kennzeichen und gelten als // Meilensteine (vergangene gelten als überfällig), chronologisch. idx verweist in plan.todos/plan.milestones, damit
// gewesen), chronologisch. idx verweist in plan.todos/plan.milestones, damit
// ein Klick denselben Dialog öffnet wie im Terminplan. // ein Klick denselben Dialog öffnet wie im Terminplan.
// Rückgabe: Array von { art, idx, titel, datum, iso, wer, meins }. // Rückgabe: Array von { art, idx, titel, datum, iso, wer, meins }.
function upcomingEntries(){ function upcomingEntries(){
@@ -126,7 +125,7 @@
meins: !!ich && (t.assignees || []).includes(ich) }); meins: !!ich && (t.assignees || []).includes(ich) });
}); });
(plan.milestones || []).forEach((m, idx) => { (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.push({ art: 'meilenstein', idx, titel: m.name || 'Meilenstein', iso: m.date, wer: [], meins: false });
}); });
rows.sort((a, b) => a.iso.localeCompare(b.iso)); rows.sort((a, b) => a.iso.localeCompare(b.iso));
+3 -3
View File
@@ -166,16 +166,16 @@
const d = parseDateISO(m.date); const d = parseDateISO(m.date);
const today = new Date(); const today = new Date();
today.setHours(0, 0, 0, 0); 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"`; const msAction = locked ? '' : ` onclick="openMilestoneModal(${idx})" title="Zum Bearbeiten klicken"`;
return `<div class="plan-task-row${locked ? ' is-locked' : ''}"${msAction}> return `<div class="plan-task-row${locked ? ' is-locked' : ''}"${msAction}>
<span class="plan-task-dot"></span> <span class="plan-task-dot"></span>
<span class="plan-task-title">${esc(m.name || 'Meilenstein')}</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>`; </div>`;
}).join(''); }).join('');
} }
const msCount = document.getElementById('planMilestoneCount'); 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 // (offen/überfällig) sind über die Kennzahlen oben erreichbar, die heute
// Abwesenden über die Kopfleiste. // Abwesenden über die Kopfleiste.
(plan.milestones || []).forEach(m => { (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)))); 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>` : ''; const gruppenkopf = gruppe !== gruppeVorher ? `<div class="bericht-rail-gruppe">${esc(gruppe)}</div>` : '';
gruppeVorher = gruppe; gruppeVorher = gruppe;
const name = (p.nr ? p.nr + ' · ' : '') + p.name; 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' : ''}" 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 || '')}" role="button" tabindex="0" data-uid="${esc(p.projectUid || '')}"
aria-current="${istAktiv ? 'true' : 'false'}"> aria-current="${istAktiv ? 'true' : 'false'}">
<span class="bericht-rail-num">${String(position).padStart(2, '0')}</span> <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>`} ${info.bucket === 'unchanged' ? '' : `<span class="bericht-rail-dot is-${esc(info.bucket)}" aria-hidden="true"></span>`}
<span class="bericht-rail-name">${esc(name)}</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>`; </div>`;
}).join(''); }).join('');
@@ -713,6 +715,7 @@ function renderBerichtDetail(p, w, prevWeek, locked){
<div class="bericht-sub">${subText}</div> <div class="bericht-sub">${subText}</div>
${fehlen.length ? `<span class="bericht-abwesend-hinweis">Heute abwesend: ${esc(fehlen.join(', '))} — eventuell vertagen.</span>` : ''} ${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 }))} ${renderBoardDrawer(p, allPeople, kLocked, w.label, Object.assign({}, BERICHT_OPTS, { ohneVerlauf: true }))}
${renderBerichtAufgaben(p, w, prevWeek, locked)}
<div class="bericht-detail-fuss"> <div class="bericht-detail-fuss">
${!locked && hatRecht('proj.plan') ? `<button type="button" class="secondary small" onclick="openTodoAusKommentar('${esc(p.id)}')"> ${!locked && hatRecht('proj.plan') ? `<button type="button" class="secondary small" onclick="openTodoAusKommentar('${esc(p.id)}')">
Aufgabe aus Kommentar… Aufgabe aus Kommentar…
@@ -876,6 +879,133 @@ function berichtVerlaufNeu(){
el.outerHTML = renderBerichtVerlauf(p, w, isWeekLocked(w)); 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 // berichtVerlaufUebernehmen: schreibt den Kommentar jener Sitzung in das
// Kommentarfeld der aktuellen (takeCommentFromHistory(), js/board/board.js). // Kommentarfeld der aktuellen (takeCommentFromHistory(), js/board/board.js).
function berichtVerlaufUebernehmen(weekLabel){ 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 { 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: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); } .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::before { display: none; }
#headerMenu.side-menu.collapsed .nav-item.nav-unterpunkt .nav-icon { display: inline-flex; } #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; }
+13 -8
View File
@@ -1250,8 +1250,8 @@ function projektStatements(){
VALUES (?, ?, ?, ?)`); VALUES (?, ?, ?, ?)`);
const insTodoDep = db.prepare(`INSERT OR IGNORE INTO plan_todo_deps (week_label, project_id, todo_id, depends_on_id) const insTodoDep = db.prepare(`INSERT OR IGNORE INTO plan_todo_deps (week_label, project_id, todo_id, depends_on_id)
VALUES (?, ?, ?, ?)`); VALUES (?, ?, ?, ?)`);
const insMilestone = db.prepare(`INSERT INTO plan_milestones (week_label, project_id, milestone_id, name, date, position, sort_order, notiz) const insMilestone = db.prepare(`INSERT INTO plan_milestones (week_label, project_id, milestone_id, name, date, position, sort_order, notiz, erreicht)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`); VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
const insMilestoneDep = db.prepare(`INSERT OR IGNORE INTO plan_milestone_deps (week_label, project_id, milestone_id, depends_on_id) const insMilestoneDep = db.prepare(`INSERT OR IGNORE INTO plan_milestone_deps (week_label, project_id, milestone_id, depends_on_id)
VALUES (?, ?, ?, ?)`); VALUES (?, ?, ?, ?)`);
@@ -1336,7 +1336,7 @@ function serializeProject(weekLabel, p, idx, nowIso){
(Array.isArray(t.dependsOn) ? t.dependsOn : []).forEach(did => insTodoDep.run(weekLabel, projId, String(t.id || ''), String(did || ''))); (Array.isArray(t.dependsOn) ? t.dependsOn : []).forEach(did => insTodoDep.run(weekLabel, projId, String(t.id || ''), String(did || '')));
}); });
(Array.isArray(plan.milestones) ? plan.milestones : []).forEach((m, midx) => { (Array.isArray(plan.milestones) ? plan.milestones : []).forEach((m, midx) => {
insMilestone.run(weekLabel, projId, String(m.id || ''), String(m.name || ''), String(m.date || ''), String(m.position || ''), midx, String(m.notiz || '').slice(0, 20000)); insMilestone.run(weekLabel, projId, String(m.id || ''), String(m.name || ''), String(m.date || ''), String(m.position || ''), midx, String(m.notiz || '').slice(0, 20000), m.erreicht ? 1 : 0);
(Array.isArray(m.dependsOn) ? m.dependsOn : []).forEach(did => insMilestoneDep.run(weekLabel, projId, String(m.id || ''), String(did || ''))); (Array.isArray(m.dependsOn) ? m.dependsOn : []).forEach(did => insMilestoneDep.run(weekLabel, projId, String(m.id || ''), String(did || '')));
}); });
} }
@@ -1564,7 +1564,7 @@ function loadWeeks(nurLabel){
const msMap = {}; const msMap = {};
milestones.forEach(r => { milestones.forEach(r => {
const k = msKey(r.week_label, r.project_id, r.milestone_id); const k = msKey(r.week_label, r.project_id, r.milestone_id);
msMap[k] = { id: r.milestone_id, name: r.name, date: r.date, position: r.position, dependsOn: [], notiz: r.notiz || '' }; msMap[k] = { id: r.milestone_id, name: r.name, date: r.date, position: r.position, dependsOn: [], notiz: r.notiz || '', erreicht: !!r.erreicht };
}); });
milestoneDeps.forEach(r => { milestoneDeps.forEach(r => {
const m = msMap[msKey(r.week_label, r.project_id, r.milestone_id)]; const m = msMap[msKey(r.week_label, r.project_id, r.milestone_id)];
@@ -1713,7 +1713,7 @@ function dbTableColumns(conn, table){
// Wiederherstellung geschrieben wird, nach einem Versionssprung nicht mehr // Wiederherstellung geschrieben wird, nach einem Versionssprung nicht mehr
// einspielbar. // einspielbar.
const IMPORT_OPTIONAL_COLS = ['updated_at', 'project_uid', 'honorar', 'status_author', 'status_at', const IMPORT_OPTIONAL_COLS = ['updated_at', 'project_uid', 'honorar', 'status_author', 'status_at',
'auftragsstatus', 'besprochen', 'leistungsphase', 'aufwand_stunden', 'notiz']; 'auftragsstatus', 'besprochen', 'leistungsphase', 'aufwand_stunden', 'notiz', 'erreicht'];
// restoreDbFromBackup: validiert die Backup-Datei (muss das bekannte Schema // restoreDbFromBackup: validiert die Backup-Datei (muss das bekannte Schema
// mit identischen Spalten wie die Live-DB besitzen) und ersetzt den // mit identischen Spalten wie die Live-DB besitzen) und ersetzt den
@@ -2730,6 +2730,12 @@ function migrateAddPlanTodoNotiz(){
db.exec("ALTER TABLE plan_milestones ADD COLUMN notiz TEXT NOT NULL DEFAULT ''"); db.exec("ALTER TABLE plan_milestones ADD COLUMN notiz TEXT NOT NULL DEFAULT ''");
console.log('plan_milestones um notiz ergänzt.'); console.log('plan_milestones um notiz ergänzt.');
} }
// erreicht: Meilenstein als erreicht/abgeschlossen markiert — sonst zählt
// ein vergangener Meilenstein auf "Meine Aufgaben" als überfällig.
if(!msCols.includes('erreicht')){
db.exec("ALTER TABLE plan_milestones ADD COLUMN erreicht INTEGER NOT NULL DEFAULT 0");
console.log('plan_milestones um erreicht ergänzt.');
}
} }
// migrateAddPlanTodoShowInGantt: rüstet die Spalte `show_in_gantt` nach // migrateAddPlanTodoShowInGantt: rüstet die Spalte `show_in_gantt` nach
@@ -5621,14 +5627,13 @@ app.get('/my-dashboard', requireAuth, (req, res) => {
AND t.project_id IN (${platzhalter}) AND t.project_id IN (${platzhalter})
`).all(weekLabel, ...meineProjekte); `).all(weekLabel, ...meineProjekte);
// Meilensteine: sie tragen kein Erledigt-Kennzeichen, deshalb zählt // Meilensteine: nur die noch nicht erreichten.
// allein das Datum.
const milestones = db.prepare(` const milestones = db.prepare(`
SELECT m.milestone_id AS id, m.name AS titel, m.date AS datum, SELECT m.milestone_id AS id, m.name AS titel, m.date AS datum,
m.project_id AS projectId, p.nr AS projectNr, p.name AS projectName m.project_id AS projectId, p.nr AS projectNr, p.name AS projectName
FROM plan_milestones m FROM plan_milestones m
JOIN projects p ON p.week_label = m.week_label AND p.project_id = m.project_id JOIN projects p ON p.week_label = m.week_label AND p.project_id = m.project_id
WHERE m.week_label = ? AND m.date != '' WHERE m.week_label = ? AND m.date != '' AND m.erreicht = 0
AND m.project_id IN (${platzhalter}) AND m.project_id IN (${platzhalter})
`).all(weekLabel, ...meineProjekte); `).all(weekLabel, ...meineProjekte);