From e12936444d0a0a2bb0be7a2c7dd187668c08394a Mon Sep 17 00:00:00 2001 From: Erik Dunin Date: Tue, 10 Nov 2026 10:41:44 +0100 Subject: [PATCH] =?UTF-8?q?Aufgaben=20im=20Wochenbericht,=20Meilensteine?= =?UTF-8?q?=20=E2=80=9EErreicht=E2=80=9C,=20Panels=20als=20Planblatt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Wochenbericht: Abschnitt „Aufgaben“ je Projekt (erledigt seit Vorwoche, überfällig, nächste zwei Wochen), rote Zahl in der Tagesordnung, Tabelle in der PDF-Akte; Klick öffnet das Aufgaben-Panel - Meilensteine lassen sich als erreicht markieren (plan_milestones.erreicht); nur nicht erreichte vergangene gelten als überfällig - Aufgabe- und Meilenstein-Panel neu aufgebaut: Stand oben, Lage im Terminplan, Angaben als Zeilen Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 49 ++++- public/App.html | 251 +++++++++++++---------- public/js/dashboard/dashboard.js | 10 +- public/js/pdf.js | 33 ++- public/js/plan/plan-gantt.js | 2 +- public/js/plan/plan-modals.js | 167 ++++++++++++++- public/js/plan/plan-sections.js | 7 +- public/js/plan/plan-tasks.js | 6 +- public/js/wand/wand.js | 2 +- public/js/wochenbericht/wochenbericht.js | 130 ++++++++++++ public/plan-styles.css | 118 +++++++++++ public/styles.css | 34 +++ server.js | 21 +- 13 files changed, 693 insertions(+), 137 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 62b0cbf..70c8fae 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 Projekt, erscheint beim Öffnen automatisch dort. Keine Daten­änderung 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“): `#todoModal` und `#milestoneModal` sind `.modal-bg.als-panel` — rechts am 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 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 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 gerissen ist. Ohne die Grenze nach vorn stünden Termine aus einem Jahr in der Liste und die dringenden gingen darin unter. -- **Meilensteine haben kein Erledigt-Kennzeichen** (`plan_milestones` hat - keine `done`-Spalte), deshalb zählt bei ihnen allein das Datum; bei Aufgaben - gilt zusätzlich `done = 0` und ein gesetztes Enddatum. +- **Meilensteine haben ein Kennzeichen „Erreicht“** (`m.erreicht`, + `plan_milestones.erreicht`, Migration in `migrateAddPlanTodoNotiz()`, in + `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 `loadGlobalPeople()`: Die globale Liste enthält nur Personen mit Benutzerkonto, die Teilnehmerliste einer Sitzung dagegen jede Person, die diff --git a/public/App.html b/public/App.html index 70a3949..2b29371 100644 --- a/public/App.html +++ b/public/App.html @@ -38,8 +38,8 @@ - - + + +
@@ -1549,51 +1575,69 @@
- -
-

Wann

-
- - +
-
-

Wer

-
- -
-
Externe Personen nicht im Projekt
-
- -
- -
-
- - optional · zählt zur Auslastung der Zuständigen +
+
+ Zeitraum +
+
+ +
+ + + + +
+
+
+ Zuständig +
+
+ +
+
Externe Personen nicht im Projekt
+
+ +
+
+
+ +
+ +
@@ -1611,41 +1655,34 @@
-
-
-

Stand

-
- - - -
-
- - 0 % -
-
-
-

Notiz

- - -
-
-
- - ▶ - Hängt ab von - 0 - -
- +
+ Hängt ab von +
+
+
+ + + Vorgänger ändern + 0 + +
+ +
-
-
-
-
+ +
+
+
+ +
+ + +
+
@@ -1963,7 +2000,7 @@ - + @@ -1977,7 +2014,7 @@ - + - + @@ -2000,14 +2037,14 @@ js/gantt.js kommen, dessen buildGanttHtml() plan-build-gantt.js umhuellt. --> - + - + - - + + @@ -2023,7 +2060,7 @@ - + diff --git a/public/js/dashboard/dashboard.js b/public/js/dashboard/dashboard.js index 033c81f..5ea11b7 100644 --- a/public/js/dashboard/dashboard.js +++ b/public/js/dashboard/dashboard.js @@ -212,14 +212,14 @@ function dashDaten(){ }); (plan.milestones || []).forEach(m => { if(!m.date) return; - termine.push({ art: 'meilenstein', titel: m.name || 'Meilenstein', datum: m.date, meins: false, wer: [] }); + termine.push({ art: 'meilenstein', titel: m.name || 'Meilenstein', datum: m.date, meins: false, wer: [], erreicht: !!m.erreicht }); }); const hatEigene = termine.some(t => t.meins); if(!rollen.length && !hatEigene) return; - // Erledigte Vergangenheit interessiert hier nicht: Meilensteine vor heute - // fallen weg (sie haben kein Erledigt-Kennzeichen), offene Aufgaben vor - // heute bleiben als überfällig stehen. - const relevant = termine.filter(t => !(t.art === 'meilenstein' && t.datum < heute)); + // Erreichte Meilensteine fallen weg (im Kalender bleiben sie stehen); + // nicht erreichte vor heute gelten wie offene Aufgaben als überfällig — + // dieselbe Regel wie die Zahl im Band (GET /my-dashboard). + const relevant = termine.filter(t => !(t.art === 'meilenstein' && t.erreicht)); relevant.sort((a, b) => (a.datum || '9999').localeCompare(b.datum || '9999')); projekte.push({ p, rollen, termine: relevant, alleTermine: termine }); }); diff --git a/public/js/pdf.js b/public/js/pdf.js index 0041e0a..931bbaf 100644 --- a/public/js/pdf.js +++ b/public/js/pdf.js @@ -518,13 +518,14 @@ function pdfWbProjektAkte(p, w, prevWeek, allePersonen){ color: '#D70015', margin: [0, 6, 0, 0] }); } - const innerContent = { + const aufgaben = pdfWbAufgaben(p, w, prevWeek); + const innerContent = { stack: [{ columns: [ { width: '*', stack: [kopf, aenderung, kommentar].concat(vorher ? [vorher] : []) }, { width: 190, stack: rightStack } ], columnGap: 12 - }; + }].concat(aufgaben ? [aufgaben] : []) }; return { table: { @@ -548,6 +549,34 @@ function pdfWbProjektAkte(p, w, prevWeek, allePersonen){ }; } +// pdfWbAufgaben: die Aufgaben der Akte wie auf dem Bildschirm +// (berichtAufgabenDaten() in wochenbericht.js) — erledigt seit der Vorwoche, +// überfällig, nächste zwei Wochen, je höchstens fünf. Rückgabe: pdfmake-Block +// oder null, wenn in keiner Gruppe etwas steht. +function pdfWbAufgaben(p, w, prevWeek){ + if(typeof berichtAufgabenDaten !== 'function') return null; + const d = berichtAufgabenDaten(p, w, prevWeek); + const gruppen = [ + [prevWeek ? 'Erledigt seit ' + berichtKurzKw(prevWeek.label) : 'Erledigt', d.erledigt, null], + ['Überfällig', d.ueberfaellig, '#D70015'], + ['Nächste zwei Wochen', d.bald, null] + ].filter(g => g[1].length); + if(!gruppen.length) return null; + const body = []; + gruppen.forEach(([titel, liste, farbe]) => { + liste.slice(0, 5).forEach((e, i) => body.push([ + { text: i === 0 ? titel + ' (' + liste.length + ')' : '', style: 'td', bold: true, color: farbe || undefined }, + { text: (e.art === 'meilenstein' ? 'Meilenstein: ' : '') + e.titel + (e.neu ? ' neu' : ''), style: 'td' }, + { text: e.kuerzel, style: 'td' }, + { text: e.datum ? formatDateDE(parseDateISO(e.datum)) : '', style: 'td', alignment: 'right', color: farbe || undefined } + ])); + }); + return { stack: [ + { text: 'AUFGABEN', fontSize: 6.5, bold: true, color: '#8E8E93', margin: [0, 10, 0, 3] }, + { table: { widths: [100, '*', 70, 50], body }, layout: pdfWbGridLayout(false) } + ] }; +} + // pdfWbVorigerKommentar: Block mit dem jüngsten früheren Kommentar des // Gewerks. Steht er in der direkten Vorwoche, heißt er "Vorwoche KW …", // sonst "Zuletzt KW …". Parameter: p (Projekt), w (Sitzung). Rückgabe: diff --git a/public/js/plan/plan-gantt.js b/public/js/plan/plan-gantt.js index 71d9ffd..fff612c 100644 --- a/public/js/plan/plan-gantt.js +++ b/public/js/plan/plan-gantt.js @@ -171,7 +171,7 @@ const openSub = overdue.length > 0 ? overdue.length + ' überfällig' : (open.length > 0 ? 'im Zeitplan' : 'alle erledigt'); const upcoming = milestones - .filter(m => m.date && parseDateISO(m.date) >= today) + .filter(m => m.date && !m.erreicht && parseDateISO(m.date) >= today) .sort((a, b) => parseDateISO(a.date) - parseDateISO(b.date)); const msZelle = upcoming.length > 0 ? bandZelle('Nächster Meilenstein', formatDateDE(parseDateISO(upcoming[0].date)), upcoming[0].name || 'Meilenstein') diff --git a/public/js/plan/plan-modals.js b/public/js/plan/plan-modals.js index c46f918..0aadac6 100644 --- a/public/js/plan/plan-modals.js +++ b/public/js/plan/plan-modals.js @@ -65,6 +65,9 @@ document.getElementById('milestoneEditDate').value = msDateIso; const msDateDisplay = document.getElementById('milestoneEditDateDisplay'); if(msDateDisplay) msDateDisplay.value = msDateIso ? formatDateDE(parseDateISO(msDateIso)) : ''; + const msErreicht = document.getElementById('milestoneEditErreicht'); + if(msErreicht) msErreicht.checked = !isNew && !!currentMs.erreicht; + panelWoSetzen('milestoneModalWo', ''); const positionSelect = document.getElementById('milestoneEditPosition'); if(positionSelect){ @@ -129,6 +132,7 @@ dependsOnList.innerHTML = listHtml; } + msLageBildNeu(); document.getElementById('milestoneModal').style.display = 'flex'; document.getElementById('milestoneEditName').focus(); }; @@ -167,6 +171,7 @@ window.closeMilestoneModal = function(){ document.getElementById('milestoneModal').style.display = 'none'; app.currentMilestoneIndex = null; + if(currentTab === 'wochenbericht') berichtAufgabenNeu(); const danach = msNachSchliessen; msNachSchliessen = null; if(danach) setTimeout(danach, 0); @@ -189,12 +194,14 @@ const position = positionSelect ? positionSelect.value : ''; const msNotizEl = document.getElementById('milestoneEditNotiz'); const notiz = msNotizEl ? msNotizEl.value.trim() : ''; + const msErreichtEl = document.getElementById('milestoneEditErreicht'); + const erreicht = !!(msErreichtEl && msErreichtEl.checked); const plan = app.ensurePlanLocal(); if(!Array.isArray(plan.milestones)) plan.milestones = []; if(app.currentMilestoneIndex === null){ - plan.milestones.push({ id: uid(), name, date, dependsOn, position, notiz }); + plan.milestones.push({ id: uid(), name, date, dependsOn, position, notiz, erreicht }); } else if(plan.milestones[app.currentMilestoneIndex]){ const m = plan.milestones[app.currentMilestoneIndex]; m.name = name; @@ -202,6 +209,7 @@ m.dependsOn = dependsOn; m.position = position; m.notiz = notiz; + m.erreicht = erreicht; } plan.milestones.sort((a,b) => parseDateISO(a.date) - parseDateISO(b.date)); app.savePlan(); @@ -266,6 +274,140 @@ return progress > 0 ? 'arbeit' : 'offen'; } + // ---------- Planblatt: Kopfzeile, Lage im Terminplan, Stand ---------- + + // kurzDatum: "2026-11-19" -> "19.11." Parameter: iso. Rückgabe: String. + function kurzDatum(iso){ return formatDateDE(parseDateISO(iso)).slice(0, 6); } + + // planKnoten: Aufgabe oder Meilenstein zu einer Knoten-id (Meilensteine mit + // Präfix "m_", wie in dependsOn). Rückgabe: {titel, start, end, ms, fertig} oder null. + function planKnoten(plan, id){ + id = String(id || ''); + if(id.indexOf('m_') === 0){ + const m = (plan.milestones || []).find(x => 'm_' + x.id === id); + return m ? { id, titel: m.name || 'Meilenstein', start: m.date || '', end: m.date || '', ms: true, fertig: !!m.erreicht } : null; + } + const t = (plan.todos || []).find(x => String(x.id) === id); + return t ? { id, titel: t.title || 'Aufgabe', start: t.start || t.end || '', end: t.end || t.start || '', ms: false, fertig: !!t.done } : null; + } + + // panelWoSetzen: Projekt (Nummer, Name) und optional Phase in der Kopfzeile. + function panelWoSetzen(elId, phase){ + const el = document.getElementById(elId); + if(!el) return; + const p = app.project || {}; + el.innerHTML = (p.nr ? `${esc(p.nr)} ` : '') + 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 '
Noch kein Termin gewählt
'; + 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 ? '' : ``; + } + const l = Math.max(0, x(a)), r = Math.min(100, x(b) + tag); + if(r <= l) return ''; + return `` + + (progress > 0 ? `` : '') + ''; + }; + 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 += ``; + const skala = [0, 1, 2, 3, 4, 5].map(i => { + const d = new Date(mo); d.setDate(d.getDate() + i * 7); + return `KW ${String(getWeekNumber(d)).padStart(2, '0')}`; + }).join(''); + const legende = []; + if(vor[0]) legende.push(`${esc(vor[0].titel)}${vor[0].fertig ? ', erledigt' : ''}`); + if(hx >= 0 && hx <= 100) legende.push(`heute ${esc(kurzDatum(heuteIso()))}`); + if(nach[0]) legende.push(`danach ${esc(nach[0].titel)} ${esc(kurzDatum(nach[0].start))}`); + return `
${skala}
${flaeche}
` + + (legende.length ? `
${legende.join('')}
` : ''); + } + + // todoLageNeu: zeichnet die Lage der Aufgabe aus den aktuellen Eingaben. + window.todoLageNeu = function(){ + const el = document.getElementById('todoLageBild'); + if(!el || !app.project) return; + const plan = app.ensurePlanLocal(); + const t = app.currentTodoIndex !== null ? plan.todos[app.currentTodoIndex] : null; + const status = currentTodoEditStatus(); + const slider = document.getElementById('todoEditProgress'); + el.innerHTML = lageBildHtml({ + id: t ? t.id : null, + start: document.getElementById('todoEditStart').value, + end: document.getElementById('todoEditEnd').value, + ms: false, fertig: status === 'fertig', + progress: status === 'fertig' ? 100 : (status === 'arbeit' && slider ? Number(slider.value) : 0), + deps: Array.from(document.querySelectorAll('#todoEditDependsOn input[type="checkbox"]:checked')).map(cb => cb.value) + }); + }; + + // msLageBildNeu: Lage, KW-Marke und Überfällig-Hinweis des Meilensteins. + window.msLageBildNeu = function(){ + const el = document.getElementById('msLageBild'); + if(!el || !app.project) return; + const plan = app.ensurePlanLocal(); + const m = app.currentMilestoneIndex !== null ? plan.milestones[app.currentMilestoneIndex] : null; + const datum = document.getElementById('milestoneEditDate').value; + const erreicht = document.getElementById('milestoneEditErreicht').checked; + const heute = heuteIso(); + const ueber = !!datum && !erreicht && datum < heute; + el.innerHTML = lageBildHtml({ + id: m ? 'm_' + m.id : null, start: datum, end: datum, ms: true, fertig: erreicht, ueber, + deps: Array.from(document.querySelectorAll('#milestoneEditDependsOn input[type="checkbox"]:checked')).map(cb => cb.value) + }); + const kw = document.getElementById('milestoneEditKw'); + const d = parseDateISO(datum); + if(kw){ kw.hidden = !d; kw.textContent = d ? 'KW ' + String(getWeekNumber(d)).padStart(2, '0') : ''; } + document.querySelectorAll('#milestoneEditStand button').forEach(b => + b.setAttribute('aria-pressed', String((b.dataset.stand === 'erreicht') === erreicht))); + const warn = document.getElementById('milestoneEditWarnung'); + if(warn){ + const tage = ueber ? daysBetween(parseDateISO(datum), parseDateISO(heute)) : 0; + warn.hidden = !ueber; + warn.textContent = ueber ? 'seit ' + (tage === 1 ? '1 Tag' : tage + ' Tagen') + ' überfällig' : ''; + } + }; + + // msStandSetzen: Umschalter "Offen | Erreicht" — schreibt das (versteckte) + // Feld #milestoneEditErreicht, das saveMilestoneEdit() liest. + window.msStandSetzen = function(erreicht){ + document.getElementById('milestoneEditErreicht').checked = !!erreicht; + msLageBildNeu(); + }; + // applyTodoStatus: schreibt den Status in das Segment und blendet den // Fortschrittsregler nur im Zustand "In Arbeit" ein (offen = 0 %, // erledigt = 100 % sind implizit). Parameter: status (String), @@ -296,6 +438,7 @@ slider.value = value; if(label) label.textContent = value + ' %'; } + todoLageNeu(); } // setTodoEditStatus: Klick-Handler des Status-Segments im TODO-Modal. @@ -313,6 +456,7 @@ todoProgressMemo = value; const label = document.getElementById('todoEditProgressVal'); if(label) label.textContent = value + ' %'; + todoLageNeu(); }; // currentTodoEditStatus: liest den aktuell gewählten Segment-Status aus dem @@ -335,6 +479,7 @@ badge.hidden = days <= 0; badge.textContent = days > 0 ? (days + ' AT') : ''; clearFieldError('todoEditRange'); + todoLageNeu(); }; // setFieldError / clearFieldError: zeigen einen Validierungshinweis direkt @@ -401,6 +546,17 @@ const count = list.querySelectorAll('input[type="checkbox"]:checked').length; badge.textContent = count; badge.classList.toggle('zero', count === 0); + const summary = document.getElementById('todoEditDependsSummary'); + if(summary){ + const plan = app.ensurePlanLocal(); + summary.innerHTML = Array.from(list.querySelectorAll('input[type="checkbox"]:checked')) + .map(cb => planKnoten(plan, cb.value)).filter(Boolean) + .map(k => `
` + + `${esc(k.titel)}` + + `${k.end ? 'bis ' + esc(kurzDatum(k.end)) : 'ohne Termin'}
`).join(''); + summary.hidden = !summary.innerHTML; + } + todoLageNeu(); }; // filterTodoDepends: blendet in der Vorgänger-Liste alle Einträge aus, die @@ -459,6 +615,8 @@ const neu = document.getElementById('todoEditSaveNew'); if(abs) abs.hidden = !an; if(wer) wer.hidden = an; + const aufwandAbs = document.getElementById('todoEditAufwandAbs'); + if(aufwandAbs) aufwandAbs.hidden = an; if(neu) neu.hidden = an; const titelEl = document.getElementById('todoModalTitle'); if(titelEl) titelEl.textContent = an ? (isNew ? 'Neue Leistungsphase' : 'Leistungsphase bearbeiten') : (isNew ? 'Neue Aufgabe' : 'Aufgabe bearbeiten'); @@ -644,6 +802,7 @@ } todoAufwandInfo(); todoPhasenModusSetzen(isNew ? !!pre.phase : !!currentTodo.lph, isNew ? null : currentTodo, isNew, isNew ? pre.lph : null); + panelWoSetzen('todoModalWo', currentTodo && typeof aufgabenPhase === 'function' ? aufgabenPhase(currentTodo, plan.todos) : ''); const storedProgress = isNew ? 0 : (parseInt(currentTodo.progress, 10) || 0); todoProgressMemo = (storedProgress > 0 && storedProgress < 100) ? storedProgress : 50; @@ -687,9 +846,10 @@ const searchInput = document.getElementById('todoEditDependsSearch'); if(searchInput) searchInput.value = ''; refreshTodoDependsCount(); - // Bereich nur aufklappen, wenn es dort auch etwas zu sehen gibt + // Gewählte Vorgänger stehen offen darüber (#todoEditDependsSummary), die + // Auswahlliste bleibt zu, bis man sie ändern will. const fold = document.getElementById('todoEditDependsFold'); - if(fold) fold.open = !!(currentTodo && Array.isArray(currentTodo.dependsOn) && currentTodo.dependsOn.length > 0); + if(fold) fold.open = false; const assigneesDiv = document.getElementById('todoEditAssignees'); if(assigneesDiv){ @@ -761,6 +921,7 @@ } document.getElementById('todoModal').style.display = 'none'; app.currentTodoIndex = null; + if(currentTab === 'wochenbericht') berichtAufgabenNeu(); todoModalSnapshot = ''; const danach = todoNachSchliessen; todoNachSchliessen = null; diff --git a/public/js/plan/plan-sections.js b/public/js/plan/plan-sections.js index d6b719d..fefff29 100644 --- a/public/js/plan/plan-sections.js +++ b/public/js/plan/plan-sections.js @@ -107,9 +107,8 @@ if(nav) nav.style.display = ''; } - // upcomingEntries: offene Aufgaben mit Endtermin und Meilensteine ab heute - // (vergangene Meilensteine haben kein Erledigt-Kennzeichen und gelten als - // gewesen), chronologisch. idx verweist in plan.todos/plan.milestones, damit + // upcomingEntries: offene Aufgaben mit Endtermin und nicht erreichte + // Meilensteine (vergangene gelten als überfällig), chronologisch. idx verweist in plan.todos/plan.milestones, damit // ein Klick denselben Dialog öffnet wie im Terminplan. // Rückgabe: Array von { art, idx, titel, datum, iso, wer, meins }. function upcomingEntries(){ @@ -126,7 +125,7 @@ meins: !!ich && (t.assignees || []).includes(ich) }); }); (plan.milestones || []).forEach((m, idx) => { - if(!m.date || m.date < heute) return; + if(!m.date || m.erreicht) return; rows.push({ art: 'meilenstein', idx, titel: m.name || 'Meilenstein', iso: m.date, wer: [], meins: false }); }); rows.sort((a, b) => a.iso.localeCompare(b.iso)); diff --git a/public/js/plan/plan-tasks.js b/public/js/plan/plan-tasks.js index 239a63d..586646f 100644 --- a/public/js/plan/plan-tasks.js +++ b/public/js/plan/plan-tasks.js @@ -166,16 +166,16 @@ const d = parseDateISO(m.date); const today = new Date(); today.setHours(0, 0, 0, 0); - const overdue = d && d < today; + const overdue = d && d < today && !m.erreicht; const msAction = locked ? '' : ` onclick="openMilestoneModal(${idx})" title="Zum Bearbeiten klicken"`; return `
${esc(m.name || 'Meilenstein')} - ${esc(d ? formatDateDE(d) : 'ohne Datum')} + ${m.erreicht ? 'erreicht · ' : ''}${esc(d ? formatDateDE(d) : 'ohne Datum')}
`; }).join(''); } const msCount = document.getElementById('planMilestoneCount'); - if(msCount) msCount.textContent = milestones.length === 0 ? '' : `${milestones.length} gesamt`; + if(msCount) msCount.textContent = milestones.length === 0 ? '' : `${milestones.filter(m => m.erreicht).length} von ${milestones.length} erreicht`; }; })(); diff --git a/public/js/wand/wand.js b/public/js/wand/wand.js index 6e00915..b015411 100644 --- a/public/js/wand/wand.js +++ b/public/js/wand/wand.js @@ -822,7 +822,7 @@ function wtTageDaten(w){ // (offen/überfällig) sind über die Kennzahlen oben erreichbar, die heute // Abwesenden über die Kopfleiste. (plan.milestones || []).forEach(m => { - if(m.date && m.date >= heute && spalteFuer[m.date]) spalteFuer[m.date].eintraege.push({ art: 'ms', titel: m.name || 'Meilenstein', projekt: hinweisFuer(m.date) + projekt, pid: pr.id, abschnitt: 'terminplan', sort: m.date }); + if(m.date && !m.erreicht && m.date >= heute && spalteFuer[m.date]) spalteFuer[m.date].eintraege.push({ art: 'ms', titel: m.name || 'Meilenstein', projekt: hinweisFuer(m.date) + projekt, pid: pr.id, abschnitt: 'terminplan', sort: m.date }); }); }); tage.forEach(t => t.eintraege.sort((a, b) => String(a.sort).localeCompare(String(b.sort)))); diff --git a/public/js/wochenbericht/wochenbericht.js b/public/js/wochenbericht/wochenbericht.js index e0a5b60..88a7797 100644 --- a/public/js/wochenbericht/wochenbericht.js +++ b/public/js/wochenbericht/wochenbericht.js @@ -666,12 +666,14 @@ function renderBerichtListe(){ const gruppenkopf = gruppe !== gruppeVorher ? `
${esc(gruppe)}
` : ''; gruppeVorher = gruppe; const name = (p.nr ? p.nr + ' · ' : '') + p.name; + const ueber = berichtAufgabenDaten(p, w, prevWeek).ueberfaellig.length; return `${gruppenkopf}
${String(position).padStart(2, '0')} ${info.bucket === 'unchanged' ? '' : ``} ${esc(name)} + ${ueber ? `${ueber}` : ''}
`; }).join(''); @@ -713,6 +715,7 @@ function renderBerichtDetail(p, w, prevWeek, locked){
${subText}
${fehlen.length ? `Heute abwesend: ${esc(fehlen.join(', '))} — eventuell vertagen.` : ''} ${renderBoardDrawer(p, allPeople, kLocked, w.label, Object.assign({}, BERICHT_OPTS, { ohneVerlauf: true }))} + ${renderBerichtAufgaben(p, w, prevWeek, locked)}
${!locked && hatRecht('proj.plan') ? `` + : `
${inhalt}
`; + }; + const spalte = (titel, liste, leer, klasse) => `
+
${esc(titel)}${liste.length}
+ ${liste.length ? liste.slice(0, BERICHT_AUFGABEN_JE_SPALTE).map(zeile).join('') : `
${esc(leer)}
`} + ${liste.length > BERICHT_AUFGABEN_JE_SPALTE ? `
${liste.length - BERICHT_AUFGABEN_JE_SPALTE} weitere
` : ''} +
`; + 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 `
+
+

Aufgaben

+ ${vorKw ? 'seit ' + esc(vorKw) : 'keine Vorwoche geladen'}${wochenberichtGewerk ? ' · Zuständige aus ' + esc(wochenberichtGewerk) + ' zuerst' : ''} + + +
+
+ ${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')} +
+ ${hinweis.length ? `
${esc(hinweis.join(' · '))}
` : ''} +
`; +} + +// berichtAufgabenNeu: zeichnet nur diesen Abschnitt und die Tagesordnung neu +// (nach dem Schließen des Panels), ein angefangener Kommentar bleibt. +function berichtAufgabenNeu(){ + const w = currentWeek(); + const p = w && aktuellesBerichtProjekt(w); + const el = document.getElementById('berichtAufgaben'); + if(!p || !el) return; + el.outerHTML = renderBerichtAufgaben(p, w, findPreviousWeek(w), isWeekLocked(w)); + renderBerichtListe(); +} + +// berichtEintragOeffnen: öffnet das Aufgaben- bzw. Meilenstein-Panel über dem +// Wochenbericht. Die Panels arbeiten auf window.PlanApp; showTab('plan') +// setzt week/project über openPlanView() ohnehin neu. +function berichtEintragOeffnen(pid, art, idx){ + const w = currentWeek(); + const p = w && (w.projects || []).find(x => x.id === pid); + if(!p) return; + window.PlanApp.week = w; + window.PlanApp.project = p; + if(art === 'meilenstein') openMilestoneModal(idx); + else openTodoModal(idx); +} + +// berichtAufgabenImProjekt: springt in den Bereich "Aufgaben" des Projekts. +function berichtAufgabenImProjekt(pid){ + location.hash = 'aufgaben'; + openProjectPlan(pid); +} + // berichtVerlaufUebernehmen: schreibt den Kommentar jener Sitzung in das // Kommentarfeld der aktuellen (takeCommentFromHistory(), js/board/board.js). function berichtVerlaufUebernehmen(weekLabel){ diff --git a/public/plan-styles.css b/public/plan-styles.css index fa8fdf4..c2a57bf 100644 --- a/public/plan-styles.css +++ b/public/plan-styles.css @@ -1872,3 +1872,121 @@ button.notizen-link, button.notizen-link:hover { all: unset; cursor: pointer; co .plan-nav-item { height: 32px; padding: 0 12px; border-bottom: 0; border-radius: 8px; font-weight: 400; display: inline-flex; align-items: center; } .plan-nav-item:hover { background: var(--surface-sunk); color: var(--text); } .plan-nav-item.is-active { background: var(--surface-sunk); color: var(--text); font-weight: 500; border-bottom-color: transparent; box-shadow: inset 0 0 0 1px var(--line-soft); } + +/* =========================================================================== + Planblatt — Seitenpanels Aufgabe und Meilenstein (Eriks Wahl, unslop, + 11/2026). Kopf: Art, Projekt, Stand und die Lage im Terminplan + (lageBildHtml() in plan-modals.js); darunter die Angaben als Zeilen mit + Beschriftung links, wie im Schriftkopf eines Plans. Ersetzt die + Abschnitte .pd-abs. Werte im 4er-Raster, neutraler Schatten. + =========================================================================== */ +.modal-bg.als-panel > .modal.pd { box-shadow: -12px 0 32px rgba(16,20,24,.14); } +@media (prefers-color-scheme: dark) { html:not([data-theme="light"]) .modal-bg.als-panel > .modal.pd { box-shadow: -12px 0 32px rgba(0,0,0,.5); } } +html[data-theme="dark"] .modal-bg.als-panel > .modal.pd { box-shadow: -12px 0 32px rgba(0,0,0,.5); } + +.pd-kopf { padding: 16px 20px 0; gap: 12px; border-bottom: 0; } +.pd-art { gap: 8px; height: 32px; font-size: 12px; font-weight: 500; min-width: 0; } +.pd-art > span { flex: none; } +.pd-art .pd-wo { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 400; color: var(--text-subtle); } +.pd-wo:not(:empty)::before { content: "· "; } +.pd-nr { font-family: var(--font-mono); font-size: 11px; } +.pd-art .modal-kbd { margin-left: 0; display: inline-flex; gap: 2px; align-items: center; } +.pd-kasten { width: 10px; height: 10px; border-radius: 3px; } +.pd-raute { width: 9px; height: 9px; } +kbd { font-family: var(--font-mono); font-size: 11px; font-weight: 400; color: var(--text-subtle); border: 1px solid var(--line); + border-bottom-width: 2px; border-radius: 4px; padding: 0 4px; line-height: 16px; background: var(--surface-alt); } +.pd-zu { width: 32px; height: 32px; border-radius: 8px; display: grid; place-items: center; } +.pd-zu svg { width: 16px; height: 16px; } + +/* Titel als Überschrift, Rahmen nur beim Zeigen und im Fokus */ +.modal-bg.als-panel .pd-kopf .pd-titel, +html.glas .modal-bg.als-panel .pd-kopf .pd-titel { + font-size: 19px; font-weight: 500; line-height: 28px; height: auto; padding: 2px 8px; margin: 0 -8px; width: calc(100% + 16px); + border: 1px solid transparent; border-radius: 8px; background: transparent; box-shadow: none; +} +.modal-bg.als-panel .pd-kopf .pd-titel:hover { border-color: var(--line-soft); } +.modal-bg.als-panel .pd-kopf .pd-titel:focus { border-color: var(--line); outline: none; background: var(--surface); } +.modal-bg.als-panel .pd-kopf .pd-titel.is-error { border-color: var(--red); } + +/* Stand direkt unter dem Titel */ +.pd-stand { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; min-height: 32px; } +.pd-stand .pd-seg { width: auto; flex: none; } +.pd-stand .pd-seg button { flex: none; min-height: 28px; padding: 0 12px; font-size: 12px; } +.pd-stand .progress-row { flex: 1; min-width: 140px; margin: 0; } +.pd-stand .progress-row output { min-width: 32px; font-family: var(--font-mono); } +#milestoneEditStand button[data-stand="offen"][aria-pressed="true"] .segmented-dot { background: var(--meilenstein); } +#milestoneEditStand button[data-stand="erreicht"][aria-pressed="true"] .segmented-dot { background: var(--green); } +.pd-warnung { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--red); } +.pd-warnung::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--red); } +.pd-warnung[hidden] { display: none; } + +/* Lage im Terminplan: sechs Kalenderwochen um den Eintrag */ +.pd-lage { margin: 0 -20px; padding: 12px 20px; background: var(--surface-alt); border-block: 1px solid var(--line-soft); display: grid; gap: 4px; } +.pd-lage-leer { font-size: 12px; color: var(--text-subtle); line-height: 20px; } +.pd-lage-skala { position: relative; height: 16px; } +.pd-lage-skala span { position: absolute; top: 0; padding-left: 4px; font-family: var(--font-mono); font-size: 11px; line-height: 16px; color: var(--text-subtle); } +.pd-lage-flaeche { position: relative; height: 52px; overflow: hidden; + background-image: repeating-linear-gradient(to right, var(--line-soft) 0 1px, transparent 1px calc(100% / 6)); } +.pd-lage-bar { position: absolute; height: 12px; border-radius: 3px; overflow: hidden; } +.pd-lage-bar.is-vor { background: color-mix(in srgb, var(--accent) 30%, var(--surface-alt)); } +.pd-lage-bar.is-fertig { background: var(--line); } +.pd-lage-bar.is-nach { background: color-mix(in srgb, var(--accent) 30%, var(--surface-alt)); } +.pd-lage-bar.is-diese { background: color-mix(in srgb, var(--accent) 24%, var(--surface)); box-shadow: inset 0 0 0 1px var(--accent); } +.pd-lage-bar.is-diese.is-fertig { background: color-mix(in srgb, var(--green) 30%, var(--surface)); box-shadow: inset 0 0 0 1px var(--green); } +.pd-lage-bar b { position: absolute; inset: 0 auto 0 0; background: var(--accent); } +.pd-lage-bar.is-fertig b { background: var(--green); } +.pd-lage-raute { position: absolute; width: 10px; height: 10px; margin-top: 1px; background: var(--meilenstein); transform: translateX(-50%) rotate(45deg); } +.pd-lage-raute.is-fertig { background: var(--text-subtle); } +.pd-lage-raute.is-diese { width: 12px; height: 12px; box-shadow: 0 0 0 2px var(--surface-alt), 0 0 0 3px var(--meilenstein); } +.pd-lage-raute.is-diese.is-fertig { background: var(--green); box-shadow: 0 0 0 2px var(--surface-alt), 0 0 0 3px var(--green); } +.pd-lage-raute.is-diese.is-ueber { background: var(--red); box-shadow: 0 0 0 2px var(--surface-alt), 0 0 0 3px var(--red); } +.pd-lage-heute { position: absolute; top: 0; bottom: 0; border-left: 1px dashed var(--text-subtle); } +.pd-lage-legende { display: flex; flex-wrap: wrap; gap: 4px 12px; font-size: 11px; color: var(--text-subtle); } +.pd-lage-legende span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; } + +/* Angaben als Zeilen */ +.pd-inhalt { padding: 4px 20px 16px; } +.pd-zeile { display: grid; grid-template-columns: 96px minmax(0, 1fr); gap: 12px; padding: 12px 0; border-bottom: 1px solid var(--line-soft); align-items: start; } +.pd-zeile:last-child { border-bottom: 0; } +.pd-zeile[hidden] { display: none; } +.pd-lab, .pd-lab label { font-size: 12px; font-weight: 400; color: var(--text-subtle); line-height: 32px; } +.pd-wert { display: grid; gap: 8px; min-width: 0; } +.pd-wert .range-picker-display, .pd-wert select { width: 100%; box-sizing: border-box; } +.pd-wert .field-inline { display: flex; align-items: center; gap: 8px; } +.pd-wert .field-inline .range-picker-display { flex: 1; min-width: 0; } +.pd-wert .field-badge { font-family: var(--font-mono); font-weight: 400; } +.pd-wert .pd-hint { font-size: 12px; } +.pd-haken { min-height: 24px; } +@media (max-width: 480px) { + .pd-zeile { grid-template-columns: minmax(0, 1fr); gap: 4px; } + .pd-lab, .pd-lab label { line-height: 20px; } +} + +/* Aufwand: Kopf entfällt (Beschriftung steht links), Werte im Raster */ +.pd-wert.pd-aufwand { margin: 0; padding: 0; border: 0; gap: 8px; } +.pd-aufwand-zeile { gap: 8px; } +.pd-stepper { height: 32px; } +.pd-stepper:focus-within { border-color: var(--accent); box-shadow: none; } +.pd-aufwand-schnell { gap: 4px; } +.pd-aufwand-schnell button { padding: 0 8px; height: 24px; line-height: 24px; border-radius: 6px; border-color: transparent; } +.pd-aufwand-schnell button[aria-pressed="true"] { font-weight: 500; border-color: transparent; } +.pd-externe { margin-top: 4px; padding-top: 8px; } + +/* Vorgänger: gewählte offen, Auswahl eingeklappt */ +.pd-vorg { display: grid; gap: 2px; } +.pd-vorg[hidden], .pd-vorg:empty { display: none; } +.pd-vorg-zeile { display: grid; grid-template-columns: 12px minmax(0, 1fr) auto; gap: 8px; align-items: center; height: 32px; padding: 0 8px; border-radius: 6px; background: var(--surface-alt); font-size: 13px; } +.pd-vorg-titel { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.pd-vorg-datum { font-family: var(--font-mono); font-size: 11px; color: var(--text-subtle); } +.pd-vorg-stand { width: 10px; height: 10px; border-radius: 3px; border: 1px solid var(--line); } +.pd-vorg-stand.is-fertig { background: var(--green); border-color: var(--green); } +.pd-vorg-stand.is-ms { border-radius: 0; transform: rotate(45deg) scale(.85); border-color: var(--meilenstein); } +.pd-vorg-stand.is-ms.is-fertig { background: var(--green); border-color: var(--green); } +.pd-wert .fold > summary { min-height: 32px; padding: 0 12px; font-size: 12px; color: var(--text-muted); } +.pd-wert .fold-caret { display: inline-grid; place-items: center; width: 16px; height: 16px; } +.pd-wert .fold-caret svg, .pd-wert .fold-search-icon svg { width: 14px; height: 14px; } +.pd-wert .fold-search-icon { display: inline-grid; place-items: center; color: var(--text-subtle); } + +.pd-notiz { min-height: 64px; padding: 8px 12px; font-size: 13px; line-height: 20px; } +.pd-stand .progress-row input[type=range] { min-width: 0; width: auto; } +.pd-fuss { padding: 12px 20px calc(12px + env(safe-area-inset-bottom, 0px)); } diff --git a/public/styles.css b/public/styles.css index 7b23298..092b86b 100644 --- a/public/styles.css +++ b/public/styles.css @@ -6489,3 +6489,37 @@ button { transition: background-color var(--dur) var(--ease-out), color var(--du #headerMenu.side-menu.collapsed .nav-item.nav-unterpunkt::before { display: none; } #headerMenu.side-menu.collapsed .nav-item.nav-unterpunkt .nav-icon { display: inline-flex; } } + +/* =========================================================================== + AUFGABEN IM WOCHENBERICHT (Eriks Wahl „B“, renderBerichtAufgaben() in + js/wochenbericht/wochenbericht.js): drei Spalten unter der Projektkarte, + Kopf wie „Verlauf der Vorwochen“ (.bv-kopf), rote Zahl in der Tagesordnung. + =========================================================================== */ +.bericht-rail-ueber { font-family: var(--font-mono); font-size: 11px; color: var(--red); font-variant-numeric: tabular-nums; flex-shrink: 0; } +.bericht-aufgaben { margin-top: 24px; padding-top: 16px; border-top: 1px solid var(--line-soft); } +.ba-spalten { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; margin-top: 4px; } +@media (max-width: 1100px) { .ba-spalten { grid-template-columns: minmax(0, 1fr); } } +.ba-spalte { display: grid; gap: 2px; align-content: start; min-width: 0; } +.ba-spalte-kopf { display: flex; justify-content: space-between; gap: 8px; font-size: 12px; color: var(--text-subtle); + padding: 0 8px 4px; border-bottom: 1px solid var(--line-soft); margin-bottom: 4px; } +.ba-zahl { font-family: var(--font-mono); font-variant-numeric: tabular-nums; color: var(--text-muted); } +.ba-spalte-kopf.is-rot .ba-zahl { color: var(--red); } +.ba-zeile { display: grid; grid-template-columns: 16px minmax(0, 1fr) auto 48px; gap: 8px; align-items: center; + min-height: 32px; padding: 0 8px; border-radius: 6px; font: inherit; font-size: 13px; color: var(--text); + background: none; border: 0; box-shadow: none; text-align: left; width: 100%; } +button.ba-zeile { cursor: pointer; transition: background-color var(--dur) var(--ease-out); } +button.ba-zeile:hover { background: var(--surface-sunk); } +button.ba-zeile:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; } +.ba-titel { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.ba-wer { font-family: var(--font-mono); font-size: 11px; color: var(--text-muted); white-space: nowrap; } +.ba-datum { font-family: var(--font-mono); font-size: 11px; color: var(--text-subtle); text-align: right; font-variant-numeric: tabular-nums; } +.ba-spalte-kopf.is-rot ~ .ba-zeile .ba-datum { color: var(--red); } +.ba-neu { font-size: 11px; color: var(--amber); margin-left: 6px; } +.ba-fort { font-family: var(--font-mono); font-size: 11px; color: var(--text-subtle); margin-left: 6px; } +.ba-haken { width: 14px; height: 14px; border-radius: 4px; border: 1px solid var(--line); display: grid; place-items: center; } +.ba-haken.is-an { background: var(--green); border-color: var(--green); } +.ba-haken.is-an::after { content: ""; width: 6px; height: 3px; border: solid var(--bg); border-width: 0 0 1.5px 1.5px; transform: translate(0, -1px) rotate(-45deg); } +.ba-haken.is-an ~ .ba-titel { color: var(--text-subtle); text-decoration: line-through; text-decoration-color: var(--line); } +.ba-raute { width: 9px; height: 9px; margin-left: 2px; background: var(--meilenstein); transform: rotate(45deg); } +.ba-leer, .ba-hinweis { font-size: 12px; color: var(--text-subtle); padding: 4px 8px; } +.ba-hinweis { padding: 8px 0 0; } diff --git a/server.js b/server.js index 41449bc..0e82b34 100644 --- a/server.js +++ b/server.js @@ -1250,8 +1250,8 @@ function projektStatements(){ VALUES (?, ?, ?, ?)`); const insTodoDep = db.prepare(`INSERT OR IGNORE INTO plan_todo_deps (week_label, project_id, todo_id, depends_on_id) VALUES (?, ?, ?, ?)`); - const insMilestone = db.prepare(`INSERT INTO plan_milestones (week_label, project_id, milestone_id, name, date, position, sort_order, notiz) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)`); + const insMilestone = db.prepare(`INSERT INTO plan_milestones (week_label, project_id, milestone_id, name, date, position, sort_order, notiz, erreicht) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`); const insMilestoneDep = db.prepare(`INSERT OR IGNORE INTO plan_milestone_deps (week_label, project_id, milestone_id, depends_on_id) 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(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 || ''))); }); } @@ -1564,7 +1564,7 @@ function loadWeeks(nurLabel){ const msMap = {}; milestones.forEach(r => { 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 => { 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 // einspielbar. 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 // 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 ''"); 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 @@ -5621,14 +5627,13 @@ app.get('/my-dashboard', requireAuth, (req, res) => { AND t.project_id IN (${platzhalter}) `).all(weekLabel, ...meineProjekte); - // Meilensteine: sie tragen kein Erledigt-Kennzeichen, deshalb zählt - // allein das Datum. + // Meilensteine: nur die noch nicht erreichten. const milestones = db.prepare(` 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 FROM plan_milestones m 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}) `).all(weekLabel, ...meineProjekte);