Compare commits
2
Commits
e291ca8438
...
v1.1.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e12936444d | ||
|
|
4ac3d8d53d |
No files matched your search
@@ -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
|
||||||
@@ -4871,6 +4914,31 @@ Kommentare nach dreimaligem Bereinigen unverändert). `entitiesReparieren()`
|
|||||||
`project_kommentare.status` und `protokoll_punkte.text` aller Sitzungen über
|
`project_kommentare.status` und `protokoll_punkte.text` aller Sitzungen über
|
||||||
`entitiesEntstapeln()` zurück.
|
`entitiesEntstapeln()` zurück.
|
||||||
|
|
||||||
|
## Behobener Fehler: Kommentare abgewählter Gewerke (11/2026)
|
||||||
|
|
||||||
|
Eriks Meldung zu 21-4030: Im Projekt-Dashboard stand ein HLS-Kommentar,
|
||||||
|
obwohl das Projekt seit KW 31 nur noch ELT hat. Ursache: Die einmalige
|
||||||
|
Umstellung `kommentareInAktuelleSitzungUebernehmen()` hatte je Gewerk den
|
||||||
|
jüngsten früheren Kommentar übernommen, ohne die aktuellen Gewerke des
|
||||||
|
Projekts zu prüfen; der Sitzungswechsel trug ihn weiter. Jetzt:
|
||||||
|
`kommentareDesProjekts()` (js/utils.js) — die Gesamtanzeige
|
||||||
|
(`kommentarVon(p, null)`, `kommentarAnzeigeHtml()`) und der Verlauf im
|
||||||
|
Projekt-Dashboard (`planKommentarDaten()`, je Sitzung mit DEREN Gewerken)
|
||||||
|
zeigen nur Gewerke des Projekts; `ensureCurrentWeekRollover()` übernimmt nur
|
||||||
|
Kommentare dieser Gewerke. Gelöscht wird nichts — die verirrte Kopie fällt
|
||||||
|
beim nächsten Sitzungswechsel aus der aktuellen Sitzung heraus.
|
||||||
|
|
||||||
|
**Darstellung mehrerer Gewerke „B – Überschrift mit Linie“** (Eriks Wahl
|
||||||
|
aus dem Artifact „Kommentare je Gewerk“): `kommentarAnzeigeHtml(p, null,
|
||||||
|
mitMeta)` zeichnet je Gewerk eine Kopfzeile (Gewerk in seiner Farbe
|
||||||
|
`--gewerk-elt/-hls/-arch`, feine Linie, mit `mitMeta` Verfasser und Zeit)
|
||||||
|
und darunter den Text; bei nur einem Gewerk wie bisher nur den Text.
|
||||||
|
Genutzt von der Kachel „Kommentar der Sitzung“ im Projekt-Dashboard (dort
|
||||||
|
entfällt dann die gemeinsame Verfasserzeile, `kommentarGewerkeMitText()`),
|
||||||
|
der Kommentaransicht der Projektliste/Wandtafel (`renderKommentarAnsicht()`)
|
||||||
|
und den Verlauf-Kacheln (ohne Verfasser). Stile `.kommentar-gewerke`,
|
||||||
|
`.kommentar-kopf` in `styles.css`.
|
||||||
|
|
||||||
## Behobene Fehler (Testlauf 10/2026, fünf Test-Agents)
|
## Behobene Fehler (Testlauf 10/2026, fünf Test-Agents)
|
||||||
|
|
||||||
- **Gesperrte Sitzungen sind für Nicht-Admins auch inhaltlich unveränderlich**:
|
- **Gesperrte Sitzungen sind für Nicht-Admins auch inhaltlich unveränderlich**:
|
||||||
|
|||||||
+110
-73
@@ -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=122">
|
<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>
|
||||||
|
<div class="pd-wert">
|
||||||
<!-- sichtbares Anzeigefeld (deutsches Format) + verstecktes ISO-Feld -->
|
<!-- 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"
|
<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' })">
|
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">
|
<input type="hidden" id="milestoneEditDate">
|
||||||
</section>
|
</div>
|
||||||
<section class="pd-abs">
|
</div>
|
||||||
<h3>Wo im Terminplan</h3>
|
<div class="pd-zeile">
|
||||||
|
<span class="pd-lab">Im Terminplan</span>
|
||||||
|
<div class="pd-wert">
|
||||||
<div class="segmented pd-seg" id="milestoneEditLage">
|
<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="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>
|
<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()">
|
<select id="milestoneEditPosition" aria-label="Position bei einer Aufgabe" onchange="msLageSync()">
|
||||||
<option value="">Oben in der Projektzeile</option>
|
<option value="">Oben in der Projektzeile</option>
|
||||||
</select>
|
</select>
|
||||||
</section>
|
</div>
|
||||||
<section class="pd-abs">
|
</div>
|
||||||
<h3>Hängt ab von</h3>
|
<div class="pd-zeile">
|
||||||
<div id="milestoneEditDependsOn" class="todo-depends-list pd-liste"></div>
|
<span class="pd-lab">Hängt ab von</span>
|
||||||
</section>
|
<div class="pd-wert">
|
||||||
<section class="pd-abs">
|
<div id="milestoneEditDependsOn" class="todo-depends-list pd-liste" onchange="msLageBildNeu()"></div>
|
||||||
<h3>Notiz</h3>
|
</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>
|
<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>
|
<textarea id="milestoneEditNotiz" class="pd-notiz" rows="3" placeholder="Hintergrund oder die Mail, aus der der Meilenstein stammt"></textarea>
|
||||||
</section>
|
</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,22 +1575,39 @@
|
|||||||
<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">
|
||||||
|
<select id="todoEditLph" onchange="todoPhaseGewechselt()"></select>
|
||||||
<p class="hint pd-hint">Aufgaben, die im Zeitraum dieser Phase beginnen, stehen im Terminplan darunter.</p>
|
<p class="hint pd-hint">Aufgaben, die im Zeitraum dieser Phase beginnen, stehen im Terminplan darunter.</p>
|
||||||
</section>
|
</div>
|
||||||
<section class="pd-abs">
|
</div>
|
||||||
<h3>Wann</h3>
|
<div class="pd-zeile">
|
||||||
|
<span class="pd-lab">Zeitraum</span>
|
||||||
|
<div class="pd-wert">
|
||||||
<div class="field-inline">
|
<div class="field-inline">
|
||||||
<input type="text" id="todoEditRange" class="range-picker-display" placeholder="Start – Ende wählen…" readonly aria-label="Zeitraum"
|
<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 })">
|
onclick="openRangePicker({ anchor: this, startInputId: 'todoEditStart', endInputId: 'todoEditEnd', displayId: 'todoEditRange', onApply: refreshTodoDuration })">
|
||||||
@@ -1573,11 +1616,12 @@
|
|||||||
<input type="hidden" id="todoEditStart">
|
<input type="hidden" id="todoEditStart">
|
||||||
<input type="hidden" id="todoEditEnd">
|
<input type="hidden" id="todoEditEnd">
|
||||||
<span class="field-error" id="todoEditRangeError" hidden></span>
|
<span class="field-error" id="todoEditRangeError" hidden></span>
|
||||||
<label class="pd-haken"><input type="checkbox" id="todoEditShowInGantt" checked> Im Terminplan als Balken zeigen</label>
|
<label class="pd-haken"><input type="checkbox" id="todoEditShowInGantt" checked> Als Balken im Terminplan</label>
|
||||||
<p class="hint pd-hint">Ohne Zeitraum erscheint die Aufgabe nur unter „Aufgaben“, nicht im Terminplan.</p>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
<section class="pd-abs" id="todoEditWerAbs">
|
<div class="pd-zeile" id="todoEditWerAbs">
|
||||||
<h3>Wer</h3>
|
<span class="pd-lab">Zuständig</span>
|
||||||
|
<div class="pd-wert">
|
||||||
<div id="todoEditAssignees" class="person-chips"></div>
|
<div id="todoEditAssignees" class="person-chips"></div>
|
||||||
<!-- Externe Personen: Kollegen mit Konto, die dem Projekt NICHT
|
<!-- Externe Personen: Kollegen mit Konto, die dem Projekt NICHT
|
||||||
zugeordnet sind (Eriks Wunsch). todoExternHinzufuegen()/
|
zugeordnet sind (Eriks Wunsch). todoExternHinzufuegen()/
|
||||||
@@ -1587,13 +1631,13 @@
|
|||||||
<div id="todoEditExterne" class="person-chips"></div>
|
<div id="todoEditExterne" class="person-chips"></div>
|
||||||
<select id="todoEditExterneWahl" aria-label="Externe Person hinzufügen" onchange="todoExternHinzufuegen(this.value)"></select>
|
<select id="todoEditExterneWahl" aria-label="Externe Person hinzufügen" onchange="todoExternHinzufuegen(this.value)"></select>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Aufwand: −/+, Schnellwahl, Umrechnung in Arbeitstage
|
<!-- Aufwand: −/+, Schnellwahl, Umrechnung in Arbeitstage
|
||||||
(todoAufwandAendern()/todoAufwandSetzen()/todoAufwandInfo(), plan-modals.js) -->
|
(todoAufwandAendern()/todoAufwandSetzen()/todoAufwandInfo(), plan-modals.js) -->
|
||||||
<div class="pd-aufwand">
|
<div class="pd-zeile" id="todoEditAufwandAbs">
|
||||||
<div class="pd-aufwand-kopf">
|
<span class="pd-lab"><label for="todoEditAufwand">Aufwand</label></span>
|
||||||
<label for="todoEditAufwand">Aufwand</label>
|
<div class="pd-wert pd-aufwand">
|
||||||
<span>optional · zählt zur Auslastung der Zuständigen</span>
|
|
||||||
</div>
|
|
||||||
<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>
|
|
||||||
<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>
|
||||||
<div class="progress-row" id="todoEditProgressRow">
|
<div class="pd-zeile">
|
||||||
<input type="range" id="todoEditProgress" min="0" max="100" step="5" value="0" aria-label="Fortschritt"
|
<span class="pd-lab">Hängt ab von</span>
|
||||||
oninput="onTodoProgressInput(this)">
|
<div class="pd-wert">
|
||||||
<output id="todoEditProgressVal">0 %</output>
|
<div class="pd-vorg" id="todoEditDependsSummary"></div>
|
||||||
</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">
|
<details class="fold" id="todoEditDependsFold">
|
||||||
<summary>
|
<summary>
|
||||||
<span class="fold-caret">▶</span>
|
<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>Hängt ab von</span>
|
<span>Vorgänger ändern</span>
|
||||||
<span class="fold-count zero" id="todoEditDependsCount">0</span>
|
<span class="fold-count zero" id="todoEditDependsCount">0</span>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="fold-body">
|
<div class="fold-body">
|
||||||
<div class="fold-search">
|
<div class="fold-search">
|
||||||
<span class="fold-search-icon">⌕</span>
|
<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()">
|
<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>
|
<div id="todoEditDependsOn" class="todo-depends-list flat" onchange="refreshTodoDependsCount()"></div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</section>
|
</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>
|
||||||
<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>
|
||||||
@@ -1954,7 +1991,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/js/globals.js?v=10"></script>
|
<script src="/js/globals.js?v=10"></script>
|
||||||
<script src="/js/utils.js?v=31"></script>
|
<script src="/js/utils.js?v=33"></script>
|
||||||
<script src="/js/icons.js?v=2"></script>
|
<script src="/js/icons.js?v=2"></script>
|
||||||
<script src="/js/ui.js?v=9"></script>
|
<script src="/js/ui.js?v=9"></script>
|
||||||
<script src="/js/date-picker.js?v=6"></script>
|
<script src="/js/date-picker.js?v=6"></script>
|
||||||
@@ -1962,8 +1999,8 @@
|
|||||||
<script src="/js/auth.js?v=11"></script>
|
<script src="/js/auth.js?v=11"></script>
|
||||||
<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=41"></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=15"></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>
|
||||||
|
|||||||
@@ -748,7 +748,7 @@ function renderBoardDrawer(p, allPeople, locked, weekLabel, opts){
|
|||||||
// renderKommentarAnsicht: der Kommentar der laufenden Sitzung, nur zu lesen.
|
// renderKommentarAnsicht: der Kommentar der laufenden Sitzung, nur zu lesen.
|
||||||
// Parameter: p (Projekt), locked. Rückgabe: HTML-String.
|
// Parameter: p (Projekt), locked. Rückgabe: HTML-String.
|
||||||
function renderKommentarAnsicht(p, locked){
|
function renderKommentarAnsicht(p, locked){
|
||||||
const text = sanitizeStatusHtml(kommentarVon(p, kommentarGewerk()).status || '');
|
const text = kommentarAnzeigeHtml(p, kommentarGewerk(), true);
|
||||||
return `<div class="status-edit is-ansicht locked-cell">${text ||
|
return `<div class="status-edit is-ansicht locked-cell">${text ||
|
||||||
'<span class="hint" style="margin:0">Für diese Sitzung ist noch kein Kommentar erfasst.</span>'}</div>`;
|
'<span class="hint" style="margin:0">Für diese Sitzung ist noch kein Kommentar erfasst.</span>'}</div>`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -93,7 +93,7 @@
|
|||||||
for(let i = versionen.length - 1; i >= 0; i--){
|
for(let i = versionen.length - 1; i >= 0; i--){
|
||||||
const k = kommentarVon(versionen[i].project, null);
|
const k = kommentarVon(versionen[i].project, null);
|
||||||
if(statusPlainText(k.status)){
|
if(statusPlainText(k.status)){
|
||||||
letzter = { week: versionen[i].week, k, aktuell: i === versionen.length - 1 };
|
letzter = { week: versionen[i].week, k, project: versionen[i].project, aktuell: i === versionen.length - 1 };
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,7 +103,8 @@
|
|||||||
gewerke.forEach(g => {
|
gewerke.forEach(g => {
|
||||||
let vorher = '';
|
let vorher = '';
|
||||||
versionen.forEach(v => {
|
versionen.forEach(v => {
|
||||||
const k = kommentarVon(v.project, g);
|
// Nur, wenn das Projekt in JENER Sitzung das Gewerk hatte.
|
||||||
|
const k = (g in kommentareDesProjekts(v.project)) ? kommentarVon(v.project, g) : { status: '' };
|
||||||
const text = statusPlainText(k.status);
|
const text = statusPlainText(k.status);
|
||||||
if(text && text !== vorher && !(letzter && v.week === letzter.week)){
|
if(text && text !== vorher && !(letzter && v.week === letzter.week)){
|
||||||
verlauf.push({ week: v.week, gewerk: g, html: sanitizeStatusHtml(k.status), autor: k.statusAuthor || '', at: k.statusAt || '' });
|
verlauf.push({ week: v.week, gewerk: g, html: sanitizeStatusHtml(k.status), autor: k.statusAuthor || '', at: k.statusAt || '' });
|
||||||
@@ -184,8 +185,9 @@
|
|||||||
if(editEl){
|
if(editEl){
|
||||||
editEl.className = 'status-edit is-ansicht';
|
editEl.className = 'status-edit is-ansicht';
|
||||||
editEl.contentEditable = 'false';
|
editEl.contentEditable = 'false';
|
||||||
// Alle Gewerke zusammen, je mit Gewerk davor (kommentarVon(), js/utils.js).
|
// Alle Gewerke zusammen; bei mehreren je mit Überschrift, Linie und
|
||||||
const text = kom.letzter ? sanitizeStatusHtml(kom.letzter.k.status) : '';
|
// Verfasser (kommentarAnzeigeHtml(), js/utils.js, Eriks Wahl „B“).
|
||||||
|
const text = kom.letzter ? kommentarAnzeigeHtml(kom.letzter.project, null, true) : '';
|
||||||
editEl.innerHTML = text || '<span class="hint" style="margin:0">Zu diesem Projekt ist noch kein Kommentar erfasst.</span>';
|
editEl.innerHTML = text || '<span class="hint" style="margin:0">Zu diesem Projekt ist noch kein Kommentar erfasst.</span>';
|
||||||
}
|
}
|
||||||
const hinweisEl = document.getElementById('planStatusHinweis');
|
const hinweisEl = document.getElementById('planStatusHinweis');
|
||||||
@@ -200,7 +202,9 @@
|
|||||||
const autorEl = document.getElementById('planStatusAutor');
|
const autorEl = document.getElementById('planStatusAutor');
|
||||||
if(autorEl){
|
if(autorEl){
|
||||||
const k = kom.letzter ? kom.letzter.k : {};
|
const k = kom.letzter ? kom.letzter.k : {};
|
||||||
autorEl.innerHTML = k.status && k.statusAuthor
|
// Bei mehreren Gewerken steht der Verfasser schon je Gewerk im Text.
|
||||||
|
const mehrere = kom.letzter && kommentarGewerkeMitText(kom.letzter.project) > 1;
|
||||||
|
autorEl.innerHTML = !mehrere && k.status && k.statusAuthor
|
||||||
? `<span class="plan-autor-ava">${esc(initialsForName(k.statusAuthor))}</span>${esc(k.statusAuthor)}`
|
? `<span class="plan-autor-ava">${esc(initialsForName(k.statusAuthor))}</span>${esc(k.statusAuthor)}`
|
||||||
+ (k.statusAt && typeof formatCommentStamp === 'function' ? ', ' + esc(formatCommentStamp(k.statusAt)) : '')
|
+ (k.statusAt && typeof formatCommentStamp === 'function' ? ', ' + esc(formatCommentStamp(k.statusAt)) : '')
|
||||||
: '';
|
: '';
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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`;
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
+39
-8
@@ -385,8 +385,21 @@ function kommentarGewerk(){
|
|||||||
// Verfasser/Zeit des jüngsten, besprochen nur, wenn ALLE Gewerke des
|
// Verfasser/Zeit des jüngsten, besprochen nur, wenn ALLE Gewerke des
|
||||||
// Projekts es abgehakt haben. Parameter: p (Projekt), g (Gewerk oder
|
// Projekts es abgehakt haben. Parameter: p (Projekt), g (Gewerk oder
|
||||||
// null/undefined). Rückgabe: { status, statusAuthor, statusAt, besprochen }.
|
// null/undefined). Rückgabe: { status, statusAuthor, statusAt, besprochen }.
|
||||||
function kommentarVon(p, g){
|
// kommentareDesProjekts: die Kommentare nur der Gewerke, die das Projekt hat.
|
||||||
|
// Ein Kommentar eines Gewerks, das dem Projekt nicht (mehr) gehört — etwa aus
|
||||||
|
// einer Zeit, als es noch HLS hatte —, gehört nicht in die Gesamtanzeige
|
||||||
|
// (Eriks Meldung zu 21-4030). Ohne gepflegte Gewerke: alle. Rückgabe: Objekt.
|
||||||
|
function kommentareDesProjekts(p){
|
||||||
const alle = (p && p.kommentare) || {};
|
const alle = (p && p.kommentare) || {};
|
||||||
|
const gewerke = Array.isArray(p && p.gewerke) ? p.gewerke : [];
|
||||||
|
if(!gewerke.length) return alle;
|
||||||
|
const aus = {};
|
||||||
|
Object.keys(alle).forEach(k => { if(gewerke.includes(k)) aus[k] = alle[k]; });
|
||||||
|
return aus;
|
||||||
|
}
|
||||||
|
|
||||||
|
function kommentarVon(p, g){
|
||||||
|
const alle = g ? ((p && p.kommentare) || {}) : kommentareDesProjekts(p);
|
||||||
if(g){
|
if(g){
|
||||||
const e = alle[g] || {};
|
const e = alle[g] || {};
|
||||||
return { status: e.status || '', statusAuthor: e.statusAuthor || '', statusAt: e.statusAt || '', besprochen: !!e.besprochen };
|
return { status: e.status || '', statusAuthor: e.statusAuthor || '', statusAt: e.statusAt || '', besprochen: !!e.besprochen };
|
||||||
@@ -408,19 +421,37 @@ function kommentarVon(p, g){
|
|||||||
}
|
}
|
||||||
|
|
||||||
// kommentarAnzeigeHtml: der Kommentar zur ANZEIGE, bereinigt. Mit Gewerk g
|
// kommentarAnzeigeHtml: der Kommentar zur ANZEIGE, bereinigt. Mit Gewerk g
|
||||||
// nur dessen Text; ohne Gewerk die Texte aller Gewerke, bei mehreren je mit
|
// nur dessen Text; ohne Gewerk die Texte aller Gewerke des Projekts. Bei
|
||||||
// dem Gewerk als eigener, dezenter Beschriftung (.kommentar-gewerk) —
|
// mehreren Gewerken Darstellung „B – Überschrift mit Linie“ (Eriks Wahl):
|
||||||
// Systemtext, nicht Teil des Kommentars (anders als die fette "ELT:"-Zeile,
|
// je Gewerk eine Kopfzeile mit dem Gewerk in seiner Farbe, einer feinen Linie
|
||||||
// die kommentarVon() für Klartext-Vorschauen einfügt). Rückgabe: HTML ('' ohne Text).
|
// und (mitMeta) Verfasser und Zeit, darunter der Text — Systemtext, nicht
|
||||||
function kommentarAnzeigeHtml(p, g){
|
// Teil des Kommentars (anders als die fette "ELT:"-Zeile, die kommentarVon()
|
||||||
|
// für Klartext-Vorschauen einfügt). Parameter: p, g, mitMeta.
|
||||||
|
// Rückgabe: HTML ('' ohne Text).
|
||||||
|
function kommentarAnzeigeHtml(p, g, mitMeta){
|
||||||
if(g) return sanitizeStatusHtml(kommentarVon(p, g).status || '');
|
if(g) return sanitizeStatusHtml(kommentarVon(p, g).status || '');
|
||||||
const alle = (p && p.kommentare) || {};
|
const alle = kommentareDesProjekts(p);
|
||||||
const reihenfolge = (typeof DEPARTMENTS === 'object' && DEPARTMENTS) ? Object.keys(DEPARTMENTS) : ['ELT','HLS','ARCH'];
|
const reihenfolge = (typeof DEPARTMENTS === 'object' && DEPARTMENTS) ? Object.keys(DEPARTMENTS) : ['ELT','HLS','ARCH'];
|
||||||
Object.keys(alle).forEach(k => { if(!reihenfolge.includes(k)) reihenfolge.push(k); });
|
Object.keys(alle).forEach(k => { if(!reihenfolge.includes(k)) reihenfolge.push(k); });
|
||||||
const mitText = reihenfolge.filter(k => alle[k] && statusPlainText(alle[k].status) !== '');
|
const mitText = reihenfolge.filter(k => alle[k] && statusPlainText(alle[k].status) !== '');
|
||||||
if(mitText.length === 0) return '';
|
if(mitText.length === 0) return '';
|
||||||
if(mitText.length === 1) return sanitizeStatusHtml(alle[mitText[0]].status);
|
if(mitText.length === 1) return sanitizeStatusHtml(alle[mitText[0]].status);
|
||||||
return mitText.map(k => `<div class="kommentar-gewerk">${esc(k)}</div><div>${sanitizeStatusHtml(alle[k].status)}</div>`).join('');
|
return '<div class="kommentar-gewerke">' + mitText.map(k => {
|
||||||
|
const e = alle[k];
|
||||||
|
const zeit = e.statusAt && typeof formatCommentStamp === 'function' ? formatCommentStamp(e.statusAt) : '';
|
||||||
|
const meta = mitMeta ? [e.statusAuthor, zeit].filter(Boolean).join(' · ') : '';
|
||||||
|
return '<div class="kommentar-block">'
|
||||||
|
+ '<div class="kommentar-kopf"><span class="kommentar-gewerk gewerk-' + esc(k.toLowerCase()) + '">' + esc(k) + '</span>'
|
||||||
|
+ '<span class="kommentar-linie"></span>' + (meta ? '<span class="kommentar-meta">' + esc(meta) + '</span>' : '') + '</div>'
|
||||||
|
+ '<div class="kommentar-text">' + sanitizeStatusHtml(e.status) + '</div></div>';
|
||||||
|
}).join('') + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// kommentarGewerkeMitText: wie viele Gewerke des Projekts einen Kommentar
|
||||||
|
// haben (für die Frage, ob eine gemeinsame Verfasserzeile Sinn ergibt).
|
||||||
|
function kommentarGewerkeMitText(p){
|
||||||
|
const alle = kommentareDesProjekts(p);
|
||||||
|
return Object.keys(alle).filter(k => alle[k] && statusPlainText(alle[k].status) !== '').length;
|
||||||
}
|
}
|
||||||
|
|
||||||
// kommentarSetzen: ändert den Kommentar-Eintrag eines Gewerks (nur die in
|
// kommentarSetzen: ändert den Kommentar-Eintrag eines Gewerks (nur die in
|
||||||
|
|||||||
@@ -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))));
|
||||||
|
|||||||
@@ -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){
|
||||||
|
|||||||
@@ -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)); }
|
||||||
+47
-6
@@ -4188,12 +4188,19 @@ tr.is-menu-open { background: var(--surface-alt); }
|
|||||||
min-height: 3em;
|
min-height: 3em;
|
||||||
}
|
}
|
||||||
.timeline-text p { margin: 0; }
|
.timeline-text p { margin: 0; }
|
||||||
/* Gewerk über dem Kommentar: Systemtext, nicht Teil des Kommentars. */
|
/* Kommentare mehrerer Gewerke, Darstellung „B – Überschrift mit Linie“
|
||||||
.kommentar-gewerk {
|
(Eriks Wahl): Gewerk in seiner Farbe, feine Linie, Verfasser und Zeit,
|
||||||
font-size: 11px; font-weight: 600; letter-spacing: 0;
|
darunter der Text. Systemtext, nicht Teil des Kommentars. */
|
||||||
color: var(--text-subtle); margin-top: 4px;
|
.kommentar-gewerke { display: flex; flex-direction: column; gap: 16px; }
|
||||||
}
|
.kommentar-block { display: flex; flex-direction: column; gap: 4px; }
|
||||||
.kommentar-gewerk:first-child { margin-top: 0; }
|
.kommentar-kopf { display: flex; align-items: center; gap: 12px; }
|
||||||
|
.kommentar-gewerk { flex: none; font-size: 12px; font-weight: 500; letter-spacing: 0; color: var(--text-muted); }
|
||||||
|
.kommentar-gewerk.gewerk-elt { color: var(--gewerk-elt); }
|
||||||
|
.kommentar-gewerk.gewerk-hls { color: var(--gewerk-hls); }
|
||||||
|
.kommentar-gewerk.gewerk-arch { color: var(--gewerk-arch); }
|
||||||
|
.kommentar-linie { flex: 1; height: 1px; background: var(--line-soft); min-width: 16px; }
|
||||||
|
.kommentar-meta { flex: none; font-size: 12px; font-weight: 400; color: var(--text-subtle); white-space: nowrap; }
|
||||||
|
:root { --gewerk-elt: var(--accent); --gewerk-hls: var(--green); --gewerk-arch: var(--amber); }
|
||||||
.timeline-leer { color: var(--text-muted); font-style: normal; }
|
.timeline-leer { color: var(--text-muted); font-style: normal; }
|
||||||
|
|
||||||
.timeline-lph-list { display: flex; flex-direction: column; gap: 1px; }
|
.timeline-lph-list { display: flex; flex-direction: column; gap: 1px; }
|
||||||
@@ -6482,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; }
|
||||||
@@ -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
|
||||||
@@ -2956,8 +2962,12 @@ function ensureCurrentWeekRollover(){
|
|||||||
// zur Sitzung und beginnt leer.
|
// zur Sitzung und beginnt leer.
|
||||||
const vorher = p.kommentare || {};
|
const vorher = p.kommentare || {};
|
||||||
p.kommentare = {};
|
p.kommentare = {};
|
||||||
|
// Nur Gewerke, die das Projekt hat — sonst lebte ein Kommentar eines
|
||||||
|
// längst abgewählten Gewerks Woche für Woche weiter (21-4030).
|
||||||
|
const gewerke = Array.isArray(p.gewerke) ? p.gewerke : [];
|
||||||
Object.keys(vorher).forEach(g => {
|
Object.keys(vorher).forEach(g => {
|
||||||
const k = vorher[g] || {};
|
const k = vorher[g] || {};
|
||||||
|
if(gewerke.length && !gewerke.includes(g)) return;
|
||||||
if(!String(k.status || '').trim()) return;
|
if(!String(k.status || '').trim()) return;
|
||||||
p.kommentare[g] = { status: k.status, statusAuthor: k.statusAuthor || '',
|
p.kommentare[g] = { status: k.status, statusAuthor: k.statusAuthor || '',
|
||||||
statusAt: k.statusAt || '', besprochen: false };
|
statusAt: k.statusAt || '', besprochen: false };
|
||||||
@@ -5617,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);
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user