Neues Design „Bauplan“, Dashboard, Notizen-Bereich, Seitenleiste

- Gestaltung „Bauplan“: ruhigere Farben, Schriftskala, Abstände im
  4-px-Raster, Kalenderwochen und Nummern in Mono-Ziffern, Glas beruhigt
- Dashboard „Meine Aufgaben“: Kopf mit Kalenderwoche, Band „Diese Woche“
  (Mo–Fr), je Projekt Meine Rolle / Team / Notizen; offene Termine blau
  hervorgehoben; Monatskalender unverändert
- Neuer Bereich „Notizen“ in der Seitenleiste: Notizen aller eigenen
  Projekte und eigene Notizen ohne Projekt, später einem Projekt zuordenbar
- Seitenleiste neu gegliedert (Projekte, Team, Notizen, Studenten,
  Einstellungen/Admin unten), Admin-Punkte in Personen/Vorgaben/System
- Reiter im Projektkopf wie die Seitenleiste hervorgehoben
- Behoben: „&…“ in Kommentaren wuchs bei jedem Speichern;
  bestehende Kommentare werden beim Start repariert
- Behoben: Lösch-Rückfrage bei Notizen unlesbar bzw. blieb stehen,
  Cursor im Notiztitel verdeckt, Dropdown im Dunkelmodus gekachelt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
erikandClaude Opus 5.5 committed 2026-11-02 15:07:32 +01:00
1 parent e697057b86
commit e291ca8438
18 files changed
+2185 -1250

No files matched your search

+58 -83
View File
@@ -38,13 +38,13 @@
<link rel="icon" type="image/png" href="/logo.png">
<link rel="alternate icon" href="/logo.ico">
<link rel="apple-touch-icon" href="/logo.png">
<link rel="stylesheet" href="/styles.css?v=111">
<link rel="stylesheet" href="/plan-styles.css?v=51">
<link rel="stylesheet" href="/styles.css?v=122">
<link rel="stylesheet" href="/plan-styles.css?v=56">
<!-- Glas-Darstellung (public/glas.css, als letztes Stylesheet): greift nur mit
der Klasse html.glas. Gesetzt wird sie hier, vor dem ersten Zeichnen,
damit die Seite nicht erst ohne Glas aufblitzt; abschaltbar je Browser
unter Einstellungen → Mein Konto (localStorage projuebersicht-glas). -->
<link rel="stylesheet" href="/glas.css?v=14">
<link rel="stylesheet" href="/glas.css?v=18">
<script>
try { if(localStorage.getItem('projuebersicht-glas') !== 'aus') document.documentElement.classList.add('glas'); }
catch(e){ document.documentElement.classList.add('glas'); }
@@ -129,116 +129,87 @@
</button>
</div>
<nav class="sidebar-nav">
<nav class="sidebar-nav" aria-label="Hauptnavigation">
<!-- Gliederung nach Arbeitsbereich (Eriks Wahl „A“, unslop-Überarbeitung
11/2026): oben das Dashboard, dann Projekte, Team, Notizen und Studenten;
Einstellungen und Admin sitzen unten (.sidebar-unten, margin-top:
auto) und rutschen nur bei aufgeklapptem Admin in den Bildlauf. -->
<div class="sidebar-gruppe">
<button class="nav-item tab active" data-tab="dashboard" onclick="showTab('dashboard')" title="Dashboard">
<span class="nav-icon"><span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="7" height="7" rx="1.5"/><path d="m4.5 7.5 1.3 1.3L8.5 6"/><line x1="13" y1="6" x2="21" y2="6"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><path d="m4.5 17.5 1.3 1.3 2.7-2.8"/><line x1="13" y1="16" x2="21" y2="16"/></svg></span></span><span class="nav-label">Dashboard</span>
</button>
</div>
<hr class="sidebar-nav-divider">
<div class="sidebar-section-label">Navigation</div>
<!-- Die eine Projektliste. Der frühere zweite Eintrag "Projekte" —
dieselben Projekte, dieselben Angaben, eine zweite Bearbeitungs-
maske — ist hier aufgegangen: Reihenfolge, Gruppenwechsel und
Löschen stehen in dieser Liste, die Projektangaben im
Projektbereich, die wöchentliche Erfassung im Wochenbericht. -->
<div class="sidebar-gruppe">
<div class="sidebar-section-label">Projekte</div>
<button class="nav-item tab" data-tab="board" onclick="showTab('board')" title="Projekte">
<span class="nav-icon"><span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2.5"/><line x1="9" y1="3" x2="9" y2="21"/><line x1="15" y1="3" x2="15" y2="21"/></svg></span></span><span class="nav-label">Projekte</span>
</button>
<!-- Geöffnetes Projekt: eingezogen unter „Projekte“, nur sichtbar,
solange ein Projekt offen ist (syncPlanNavItem(), js/tabs-auth.js). -->
<button class="nav-item tab nav-unterpunkt" data-tab="plan" id="planNavItem" style="display:none" onclick="showTab('plan')" title="Projektbereich">
<span class="nav-icon"><span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="5" width="17" height="14" rx="2"/><line x1="3.5" y1="9.5" x2="20.5" y2="9.5"/><line x1="7" y1="13" x2="13" y2="13"/><line x1="7" y1="16" x2="16" y2="16"/></svg></span></span><span class="nav-label" id="planNavItemLabel">Projektbereich</span>
</button>
<button class="nav-item tab" data-tab="wochenbericht" onclick="showTab('wochenbericht')" title="Wochenbericht">
<span class="nav-icon"><span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3.5h8a1.5 1.5 0 0 1 1.5 1.5v14a1.5 1.5 0 0 1-1.5 1.5H8A1.5 1.5 0 0 1 6.5 19V5A1.5 1.5 0 0 1 8 3.5Z"/><path d="M9.5 3.5h5v2.2a.8.8 0 0 1-.8.8h-3.4a.8.8 0 0 1-.8-.8V3.5Z"/><path d="m9.3 12.2 1.5 1.5 3.4-3.4"/><line x1="9.3" y1="16.3" x2="14.7" y2="16.3"/></svg></span></span><span class="nav-label">Wochenbericht</span>
</button>
<button class="nav-item tab" data-tab="load" onclick="showTab('load')" title="Auslastung">
<span class="nav-icon"><span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="21" x2="5" y2="10"/><line x1="12" y1="21" x2="12" y2="5"/><line x1="19" y1="21" x2="19" y2="14"/><line x1="3" y1="21" x2="21" y2="21"/></svg></span></span><span class="nav-label">Auslastung</span>
</button>
<!-- Abwesenheiten (js/abwesenheit/abwesenheit.js): sichtbar mit einem der
vier Abwesenheits-Rechte (data-recht mit "|" = eines davon genügt). -->
<button class="nav-item tab nur-recht" data-recht="team.abw|team.abwProjekte|team.abwGewerk|team.abwAlle" data-tab="abwesenheit" onclick="showTab('abwesenheit')" title="Abwesenheiten">
<span class="nav-icon"><span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="5" width="17" height="15.5" rx="2"/><line x1="3.5" y1="9.5" x2="20.5" y2="9.5"/><line x1="8" y1="3" x2="8" y2="6.5"/><line x1="16" y1="3" x2="16" y2="6.5"/><line x1="8.5" y1="13" x2="15.5" y2="17.5"/><line x1="15.5" y1="13" x2="8.5" y2="17.5"/></svg></span></span><span class="nav-label">Abwesenheiten</span>
</button>
<!-- "Projektbeteiligte" stand hier bis zur Entfernung der Abteilungs-
trennung (migrateRemoveDepartmentPartition(), CLAUDE.md
"Departments were retired"): eine rein lesende Liste, deren
eigentlicher Zweck — Beteiligte gezielt aus einer ANDEREN Abteilung
in die eigene Sitzung zu holen — mit der Zusammenlegung zu EINEM
gemeinsamen Personenkreis entfiel. Übrig blieb eine Dopplung der
Rollen-/Avatar-Chips, die in Wochenbericht/Board/Auslastung ohnehin
schon sichtbar sind. Auf Eriks Wunsch aus der Seitenleiste entfernt;
der Tab selbst (`#people`, `js/people/people.js`) und sein Slug
`Projektbeteiligte` in VALID_TAB_SLUGS bestehen unverändert fort —
ein alter Link/Deep-Link funktioniert also weiterhin, nur der
direkte Weg über die Navigation fehlt. -->
<!-- Projektbereich: der einzige Navigationspunkt, der nicht immer da
ist. Er erscheint, sobald ein Projekt geöffnet wurde, und trägt
dessen Bezeichnung (syncPlanNavItem() in js/tabs-auth.js). Ohne
ihn wäre im Projektbereich gar kein Punkt hervorgehoben und die
Seitenleiste zeigte nicht mehr, wo man gerade ist. -->
<button class="nav-item tab" data-tab="plan" id="planNavItem" style="display:none" onclick="showTab('plan')" title="Projektbereich">
<span class="nav-icon"><span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="5" width="17" height="14" rx="2"/><line x1="3.5" y1="9.5" x2="20.5" y2="9.5"/><line x1="7" y1="13" x2="13" y2="13"/><line x1="7" y1="16" x2="16" y2="16"/></svg></span></span><span class="nav-label" id="planNavItemLabel">Projektbereich</span>
</button>
<!-- Planung: eigener, zweiter Abschnitt (nicht Teil von "Navigation"),
wie gewünscht — eine Sandbox für Terminpläne unabhängig von einem
echten Projekt (aus Honorar errechnet oder frei angelegt), siehe
CLAUDE.md "Planung — projektunabhängige Terminplan-Sandbox". -->
<div class="sidebar-section-label nur-recht" data-recht="wz.planung">Planung</div>
<button class="nav-item tab nur-recht" data-recht="wz.planung" data-tab="planung" onclick="showTab('planung')" title="Terminplan">
<span class="nav-icon"><span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="4" width="17" height="17" rx="2.5"/><line x1="3.5" y1="9.5" x2="20.5" y2="9.5"/><line x1="8" y1="2.5" x2="8" y2="6.5"/><line x1="16" y1="2.5" x2="16" y2="6.5"/><line x1="7" y1="14" x2="12" y2="14"/><line x1="7" y1="17.5" x2="15" y2="17.5"/></svg></span></span><span class="nav-label">Terminplan</span>
</button>
</div>
<!-- Studenten: eigener, dritter Abschnitt (wie "Planung" nicht Teil
von "Navigation") — bislang nur ein Werkzeug, der Plan Checker:
lädt einen Bauplan als PDF hoch und lässt ihn von einer KI gegen
eine Liste von Merkmalen prüfen (Maßketten, Plankopf, ...),
siehe js/planchecker/planchecker.js. -->
<div class="sidebar-section-label nur-recht" data-recht="wz.checker">Studenten</div>
<div class="sidebar-gruppe">
<div class="sidebar-section-label">Team</div>
<button class="nav-item tab" data-tab="load" onclick="showTab('load')" title="Auslastung">
<span class="nav-icon"><span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="21" x2="5" y2="10"/><line x1="12" y1="21" x2="12" y2="5"/><line x1="19" y1="21" x2="19" y2="14"/><line x1="3" y1="21" x2="21" y2="21"/></svg></span></span><span class="nav-label">Auslastung</span>
</button>
<button class="nav-item tab nur-recht" data-recht="team.abw|team.abwProjekte|team.abwGewerk|team.abwAlle" data-tab="abwesenheit" onclick="showTab('abwesenheit')" title="Abwesenheiten">
<span class="nav-icon"><span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="5" width="17" height="15.5" rx="2"/><line x1="3.5" y1="9.5" x2="20.5" y2="9.5"/><line x1="8" y1="3" x2="8" y2="6.5"/><line x1="16" y1="3" x2="16" y2="6.5"/><line x1="8.5" y1="13" x2="15.5" y2="17.5"/><line x1="15.5" y1="13" x2="8.5" y2="17.5"/></svg></span></span><span class="nav-label">Abwesenheiten</span>
</button>
</div>
<div class="sidebar-gruppe">
<div class="sidebar-section-label">Notizen</div>
<button class="nav-item tab" data-tab="notizen" onclick="showTab('notizen')" title="Notizen">
<span class="nav-icon"><span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M6.5 3.5h7l4 4v12a1 1 0 0 1-1 1h-10a1 1 0 0 1-1-1v-15a1 1 0 0 1 1-1Z"/><path d="M13.5 3.5V7a1 1 0 0 0 1 1h4"/><line x1="8.5" y1="12.5" x2="15.5" y2="12.5"/><line x1="8.5" y1="16" x2="13.5" y2="16"/></svg></span></span><span class="nav-label">Notizen</span>
</button>
</div>
<div class="sidebar-gruppe nur-recht" data-recht="wz.checker">
<div class="sidebar-section-label">Studenten</div>
<button class="nav-item tab nur-recht" data-recht="wz.checker" data-tab="planchecker" onclick="showTab('planchecker')" title="Plan Checker">
<span class="nav-icon"><span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="3" width="16" height="18" rx="2"/><path d="m9 12 2 2 4-4"/></svg></span></span><span class="nav-label">Plan Checker</span>
</button>
</div>
<hr class="sidebar-nav-divider">
<!-- Einstellungen: seit die firmenweiten Masken in den Admin-Bereich
gewandert sind, steht hier nur noch das eigene Konto — ein
Aufklapp-Menue fuer einen einzigen Punkt waere Ballast. -->
<div class="sidebar-gruppe sidebar-unten">
<button class="nav-item tab" data-tab="settings" onclick="showTab('settings')" title="Einstellungen">
<span class="nav-icon"><span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3.2"/><path d="M12 3.5v2.4M12 18.1v2.4M20.5 12h-2.4M5.9 12H3.5M17.7 6.3l-1.7 1.7M8 15.7l-1.7 1.7M17.7 17.7 16 16M8 8 6.3 6.3"/></svg></span></span><span class="nav-label">Einstellungen</span>
</button>
<!-- Der Bereich „Werkzeuge“ ist entfallen (Eriks Wunsch): „+ Neues
Projekt“ steht in der Projektliste („+ Projekt“), die Wandtafel
braucht nur das Wandtafel-Konto (landet dort automatisch, sonst
über /Wandtafel erreichbar). -->
<!-- Admin: aufklappbare Gruppe ueber die Abschnitte von #admin. Wie
bei "Einstellungen" zuvor ist das KEINE Aktionsliste, sondern
Navigation innerhalb eines Bereichs — deshalb fuehrt schon der
Kopf-Eintrag dorthin (toggleAdminNav()) und die Unterpunkte
schalten nur den sichtbaren Abschnitt um (showAdminSection() in
js/admin/admin.js). Die gesamte Gruppe blendet renderUserBar()
(js/auth.js) fuer Nicht-Administratoren aus; die eigentliche
Sperre sitzt serverseitig auf den Routen. -->
<div id="navAdminSection" style="display:none">
<button class="nav-item nav-group-toggle" id="adminAccordionToggle" onclick="toggleAdminNav()" title="Admin" aria-expanded="false" aria-controls="adminSubnav">
<span class="nav-icon"><span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3.5 5.5 6v5.3c0 4.4 2.9 7.4 6.5 8.7 3.6-1.3 6.5-4.3 6.5-8.7V6Z"/><path d="m9 12 2.1 2.1L15.5 9.5"/></svg></span></span><span class="nav-label">Admin</span>
<span class="nav-chevron"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></span>
</button>
<div class="sidebar-subnav" id="adminSubnav">
<div class="sidebar-subgruppe">Personen</div>
<button class="menu-item sidebar-subitem" data-admin-section="benutzer" onclick="showAdminSection('benutzer')">Benutzerverwaltung</button>
<button class="menu-item sidebar-subitem" data-admin-section="rollen" onclick="showAdminSection('rollen')">Rollen &amp; Rechte</button>
<div class="sidebar-subgruppe">Vorgaben</div>
<button class="menu-item sidebar-subitem" data-admin-section="honorar" onclick="showAdminSection('honorar')">Honorar</button>
<button class="menu-item sidebar-subitem" data-admin-section="auslastung" onclick="showAdminSection('auslastung')">Auslastung</button>
<button class="menu-item sidebar-subitem" data-admin-section="termine" onclick="showAdminSection('termine')">Termine &amp; Sitzungen</button>
<button class="menu-item sidebar-subitem" data-admin-section="feiertage" onclick="showAdminSection('feiertage')">Feiertage</button>
<button class="menu-item sidebar-subitem" data-admin-section="vorlagen" onclick="showAdminSection('vorlagen')">Aufgaben-Vorlagen</button>
<button class="menu-item sidebar-subitem" data-admin-section="planchecker" onclick="showAdminSection('planchecker')">Plan-Checker-Merkmale</button>
<div class="sidebar-subgruppe">System</div>
<button class="menu-item sidebar-subitem" data-admin-section="betrieb" onclick="showAdminSection('betrieb')">Betrieb &amp; Sicherheit</button>
<button class="menu-item sidebar-subitem" data-admin-section="teams" onclick="showAdminSection('teams')">Teams</button>
<button class="menu-item sidebar-subitem" data-admin-section="papierkorb" onclick="showAdminSection('papierkorb')">Papierkorb</button>
<button class="menu-item sidebar-subitem" data-admin-section="datenbank" onclick="showAdminSection('datenbank')">Datenbank &amp; Sitzungen</button>
</div>
</div>
</div>
</nav>
<button class="sidebar-collapse-bar" id="sidebarCollapseToggle" onclick="toggleSidebarCollapse()" title="Seitenleiste ein-/ausklappen" aria-pressed="false">
@@ -299,6 +270,9 @@
<!-- LOAD -->
<!-- ABWESENHEITEN (js/abwesenheit/abwesenheit.js) -->
<div id="abwesenheit" style="display:none"></div>
<!-- NOTIZEN (js/notizen/notizen.js): Notizen aller eigenen Projekte und
projektunabhängige Notizen -->
<div id="notizen" style="display:none"></div>
<div id="load" style="display:none">
<div class="section-title">Auslastung der Projektbeteiligten</div>
@@ -1979,14 +1953,14 @@
</main>
</div>
<script src="/js/globals.js?v=9"></script>
<script src="/js/globals.js?v=10"></script>
<script src="/js/utils.js?v=31"></script>
<script src="/js/icons.js?v=2"></script>
<script src="/js/ui.js?v=9"></script>
<script src="/js/date-picker.js?v=6"></script>
<script src="/js/server.js?v=7"></script>
<script src="/js/auth.js?v=11"></script>
<script src="/js/store.js?v=16"></script>
<script src="/js/store.js?v=17"></script>
<script src="/js/weeks.js?v=12"></script>
<script src="/js/board/board.js?v=41"></script>
<script src="/js/wochenbericht/wochenbericht.js?v=29"></script>
@@ -1994,6 +1968,7 @@
<script src="/js/planung/planung.js?v=3"></script>
<script src="/js/planchecker/planchecker.js?v=1"></script>
<script src="/js/abwesenheit/abwesenheit.js?v=10"></script>
<script src="/js/notizen/notizen.js?v=3"></script>
<script src="/js/projects/projects.js?v=10"></script>
<script src="/js/load/load.js?v=23"></script>
<!-- auftrag.js: Auftragsstatus und Dialog "Beauftragung". Nach load.js, weil
@@ -2002,7 +1977,7 @@
<!-- Assistent "Neues Projekt": braucht auftrag.js (Auftragsstatus) und
load.js (faktorFormat), steht deshalb dahinter. -->
<script src="/js/projekt-wizard.js?v=6"></script>
<script src="/js/dashboard/dashboard.js?v=11"></script>
<script src="/js/dashboard/dashboard.js?v=13"></script>
<script src="/js/benachrichtigungen.js?v=5"></script>
<!-- wand.js braucht berechneAuslastung()/loadAmpelColor()/kapazitaetsFaktor()/
stundenFormat() (load.js), classifyProjectChange()/findPreviousWeek()
@@ -2012,10 +1987,10 @@
Tab-Ordner, keine neue Abhängigkeit nach vorn. -->
<script src="/js/wand/wand.js?v=21"></script>
<script src="/js/settings/settings.js?v=14"></script>
<script src="/js/admin/admin.js?v=23"></script>
<script src="/js/admin/admin.js?v=24"></script>
<script src="/js/admin-users.js?v=10"></script>
<script src="/js/admin/rollen.js?v=1"></script>
<script src="/js/einfuehrung.js?v=2"></script>
<script src="/js/einfuehrung.js?v=3"></script>
<script src="/js/gantt.js?v=10"></script>
<!-- Module des Projektbereichs (#plan). Sie liefen frueher nur in
project-plan.html; seit der Projektbereich ein Bereich dieser Seite ist,
@@ -2029,25 +2004,25 @@
<script src="/js/plan/plan-info.js?v=15"></script>
<script src="/js/plan/plan-history.js?v=4"></script>
<script src="/js/plan/plan-build-gantt.js?v=11"></script>
<script src="/js/plan/plan-gantt.js?v=22"></script>
<script src="/js/plan/plan-gantt.js?v=23"></script>
<script src="/js/plan/plan-drag.js?v=8"></script>
<script src="/js/plan/plan-modals.js?v=18"></script>
<script src="/js/plan/plan-tasks.js?v=4"></script>
<script src="/js/plan/plan-phasen.js?v=3"></script>
<script src="/js/plan/plan-files.js?v=4"></script>
<script src="/js/plan/plan-protokoll.js?v=20"></script>
<script src="/js/plan/plan-protokoll.js?v=21"></script>
<script src="/js/plan/plan-templates.js?v=11"></script>
<script src="/js/plan/plan-protokoll-todos.js?v=2"></script>
<script src="/js/plan/plan-protokoll-todos.js?v=3"></script>
<script src="/js/plan/plan-mail-eintrag.js?v=4"></script>
<script src="/js/plan/plan-notizen.js?v=1"></script>
<script src="/js/plan/plan-notizen.js?v=2"></script>
<script src="/js/plan/plan-honorar.js?v=9"></script>
<!-- plan-leistung.js muss NACH plan-honorar.js stehen: es umhuellt dessen
invalidateHonorarConfig(), damit eine Aenderung der Anteile auch seinen
Zwischenspeicher verwirft. -->
<script src="/js/plan/plan-leistung.js?v=1"></script>
<script src="/js/plan/plan-settings.js?v=14"></script>
<script src="/js/plan/plan-schedule.js?v=6"></script>
<script src="/js/tabs-auth.js?v=17"></script>
<script src="/js/plan/plan-schedule.js?v=7"></script>
<script src="/js/tabs-auth.js?v=19"></script>
<script src="/js/pdf.js?v=31"></script>
<script src="/js/backup.js?v=2"></script>
<script src="/js/import.js?v=1"></script>
+96 -89
View File
@@ -1,5 +1,8 @@
/* ===========================================================================
GLAS-DARSTELLUNG (Eriks Wahl: Gestaltungsvorschlag "Projektübersicht in
GLAS-DARSTELLUNG — beruhigt in der unslop-Überarbeitung „Bauplan“ (Eriks
Wahl „Beruhigen“): Flächen fast deckend, Lichtflächen schwach und nur noch
in den zwei Logo-Blautönen, kleinere Rundungen und neutrale Schatten.
Ursprünglich: (Eriks Wahl: Gestaltungsvorschlag "Projektübersicht in
Glas", Annäherung an Apple Liquid Glass)
---------------------------------------------------------------------------
Eine eigene Schicht über styles.css und plan-styles.css, geladen als LETZTES
@@ -27,107 +30,105 @@
/* ---------- Tokens: hell ---------- */
html.glas {
--bg: #e8eef3;
--surface: rgba(255,255,255,.58);
--surface-alt: rgba(255,255,255,.40);
--surface-sunk: rgba(15,90,133,.07);
--line: rgba(20,45,65,.16);
--line-soft: rgba(20,45,65,.09);
--bg: #eef1f4;
--surface: rgba(255,255,255,.82);
--surface-alt: rgba(255,255,255,.66);
--surface-sunk: rgba(16,24,32,.05);
--line: rgba(16,24,32,.13);
--line-soft: rgba(16,24,32,.07);
--accent: #0f5a85;
--accent-hover: #0d5079;
--accent-active: #0a4565;
--accent-soft: rgba(15,90,133,.11);
--accent-soft: rgba(15,90,133,.09);
--band-neu: #0f5a85;
--text: #14212b;
--text-muted: #4b5d6b;
--text-subtle: #6f8291;
--text: #16191d;
--text-muted: #4d545c;
--text-subtle: #69707a;
--r-md: 10px;
--r-lg: 18px;
--r-xl: 22px;
--r-md: 8px;
--r-lg: 14px;
--r-xl: 16px;
--sh-1: 0 8px 28px rgba(21,58,86,.10), 0 1px 4px rgba(21,58,86,.06);
--sh-2: 0 12px 34px rgba(21,58,86,.13), 0 2px 6px rgba(21,58,86,.07);
--sh-3: 0 20px 60px rgba(21,58,86,.20), 0 4px 14px rgba(21,58,86,.10);
--sh-1: 0 1px 2px rgba(16,24,32,.05);
--sh-2: 0 4px 16px rgba(16,24,32,.07), 0 1px 2px rgba(16,24,32,.04);
--sh-3: 0 12px 32px rgba(16,24,32,.12), 0 2px 6px rgba(16,24,32,.06);
--glas-rahmen: rgba(255,255,255,.62);
--glas-dicht: rgba(255,255,255,.86);
--glas-rahmen: rgba(255,255,255,.84);
--glas-dicht: rgba(255,255,255,.94);
--glas-deckend: #f7fafc;
--glas-kante: rgba(255,255,255,.80);
--glas-kante-2: rgba(255,255,255,.35);
--glas-blur: 24px;
--glas-kante: rgba(255,255,255,.70);
--glas-kante-2: rgba(255,255,255,.25);
--glas-blur: 16px;
--glas-hell: #94b0cc;
--glas-tief: #116896;
--glas-logo: .16;
--glas-logo: .07;
}
/* ---------- Tokens: dunkel (System und manuell) ---------- */
@media (prefers-color-scheme: dark) {
html.glas:not([data-theme="light"]) {
--bg: #0b141b;
--surface: rgba(30,46,60,.50);
--surface-alt: rgba(30,46,60,.34);
--surface-sunk: rgba(255,255,255,.06);
--line: rgba(255,255,255,.14);
--line-soft: rgba(255,255,255,.08);
--accent: #8cc9e6;
--accent-hover: #a3d4ec;
--accent-active: #6fb3d3;
--accent-soft: rgba(140,201,230,.15);
--bg: #111315;
--surface: rgba(26,29,33,.80);
--surface-alt: rgba(26,29,33,.62);
--surface-sunk: rgba(255,255,255,.05);
--line: rgba(236,238,240,.13);
--line-soft: rgba(236,238,240,.07);
--accent: #86bfdf;
--accent-hover: #9dcbe5;
--accent-active: #6aa9cc;
--accent-soft: rgba(134,191,223,.13);
--band-neu: #116896;
--text: #e8f0f5;
--text-muted: #a9bccb;
--text-subtle: #7790a2;
--sh-1: 0 10px 30px rgba(0,0,0,.40), 0 1px 4px rgba(0,0,0,.30);
--sh-2: 0 14px 38px rgba(0,0,0,.45), 0 2px 8px rgba(0,0,0,.30);
--sh-3: 0 22px 64px rgba(0,0,0,.55), 0 4px 16px rgba(0,0,0,.35);
--glas-rahmen: rgba(28,42,54,.58);
--glas-dicht: rgba(22,34,44,.90);
--glas-deckend: #15212b;
--glas-kante: rgba(255,255,255,.18);
--glas-kante-2: rgba(255,255,255,.07);
--text: #eceef0;
--text-muted: #a5abb2;
--text-subtle: #878d95;
--sh-1: 0 1px 2px rgba(0,0,0,.4);
--sh-2: 0 4px 16px rgba(0,0,0,.45);
--sh-3: 0 16px 40px rgba(0,0,0,.55), 0 2px 8px rgba(0,0,0,.35);
--glas-rahmen: rgba(24,27,30,.84);
--glas-dicht: rgba(22,24,27,.95);
--glas-deckend: #181a1d;
--glas-kante: rgba(255,255,255,.12);
--glas-kante-2: rgba(255,255,255,.05);
--glas-hell: #2c5675;
--glas-tief: #1d6f99;
--glas-logo: .10;
--glas-logo: .06;
}
}
html.glas[data-theme="dark"] {
--bg: #0b141b;
--surface: rgba(30,46,60,.50);
--surface-alt: rgba(30,46,60,.34);
--surface-sunk: rgba(255,255,255,.06);
--line: rgba(255,255,255,.14);
--line-soft: rgba(255,255,255,.08);
--accent: #8cc9e6;
--accent-hover: #a3d4ec;
--accent-active: #6fb3d3;
--accent-soft: rgba(140,201,230,.15);
--bg: #111315;
--surface: rgba(26,29,33,.80);
--surface-alt: rgba(26,29,33,.62);
--surface-sunk: rgba(255,255,255,.05);
--line: rgba(236,238,240,.13);
--line-soft: rgba(236,238,240,.07);
--accent: #86bfdf;
--accent-hover: #9dcbe5;
--accent-active: #6aa9cc;
--accent-soft: rgba(134,191,223,.13);
--band-neu: #116896;
--text: #e8f0f5;
--text-muted: #a9bccb;
--text-subtle: #7790a2;
--sh-1: 0 10px 30px rgba(0,0,0,.40), 0 1px 4px rgba(0,0,0,.30);
--sh-2: 0 14px 38px rgba(0,0,0,.45), 0 2px 8px rgba(0,0,0,.30);
--sh-3: 0 22px 64px rgba(0,0,0,.55), 0 4px 16px rgba(0,0,0,.35);
--glas-rahmen: rgba(28,42,54,.58);
--glas-dicht: rgba(22,34,44,.90);
--glas-deckend: #15212b;
--glas-kante: rgba(255,255,255,.18);
--glas-kante-2: rgba(255,255,255,.07);
--text: #eceef0;
--text-muted: #a5abb2;
--text-subtle: #878d95;
--sh-1: 0 1px 2px rgba(0,0,0,.4);
--sh-2: 0 4px 16px rgba(0,0,0,.45);
--sh-3: 0 16px 40px rgba(0,0,0,.55), 0 2px 8px rgba(0,0,0,.35);
--glas-rahmen: rgba(24,27,30,.84);
--glas-dicht: rgba(22,24,27,.95);
--glas-deckend: #181a1d;
--glas-kante: rgba(255,255,255,.12);
--glas-kante-2: rgba(255,255,255,.05);
--glas-hell: #2c5675;
--glas-tief: #1d6f99;
--glas-logo: .10;
--glas-logo: .06;
}
/* ---------- Hintergrund: Lichtflächen und Logo ---------- */
html.glas body {
background:
radial-gradient(55% 50% at 8% 4%, color-mix(in srgb, var(--glas-hell) 70%, transparent), transparent 70%),
radial-gradient(45% 45% at 92% 12%, color-mix(in srgb, var(--glas-tief) 26%, transparent), transparent 70%),
radial-gradient(50% 55% at 72% 96%, color-mix(in srgb, #7fc3b6 26%, transparent), transparent 70%),
radial-gradient(40% 40% at 28% 82%, color-mix(in srgb, var(--glas-hell) 42%, transparent), transparent 70%),
radial-gradient(60% 55% at 8% 4%, color-mix(in srgb, var(--glas-hell) 26%, transparent), transparent 70%),
radial-gradient(50% 50% at 92% 12%, color-mix(in srgb, var(--glas-tief) 9%, transparent), transparent 70%),
var(--bg);
background-attachment: fixed;
}
@@ -157,6 +158,7 @@ html.glas .abw-team-kopf,
html.glas .report-card,
html.glas .load-card,
html.glas .settings-card,
html.glas .mn-notizen,
html.glas .dash-karte,
html.glas .dash-band,
html.glas .bericht-panel,
@@ -181,8 +183,8 @@ html.glas .plan-toast,
html.glas .glocke-menue,
html.glas .wiz {
background: var(--surface);
-webkit-backdrop-filter: blur(var(--glas-blur)) saturate(170%);
backdrop-filter: blur(var(--glas-blur)) saturate(170%);
-webkit-backdrop-filter: blur(var(--glas-blur));
backdrop-filter: blur(var(--glas-blur));
border: 1px solid var(--glas-kante-2);
box-shadow: var(--sh-1), inset 0 1px 0 var(--glas-kante), inset 0 -1px 0 var(--glas-kante-2);
}
@@ -191,7 +193,7 @@ html.glas .wiz {
html.glas .app-topbar {
background: var(--glas-rahmen);
border-width: 0 0 1px 0;
box-shadow: 0 1px 0 var(--glas-kante-2), 0 6px 24px rgba(21,58,86,.06);
box-shadow: 0 1px 0 var(--glas-kante-2), 0 6px 24px rgba(16,24,32,.06);
}
html.glas .topbar-logo { height: 26px; }
@@ -248,11 +250,11 @@ html.glas .modal-bg {
margin: 12px 12px 0;
border-radius: var(--r-xl);
background: var(--glas-rahmen);
-webkit-backdrop-filter: blur(var(--glas-blur)) saturate(170%);
backdrop-filter: blur(var(--glas-blur)) saturate(170%);
-webkit-backdrop-filter: blur(var(--glas-blur));
backdrop-filter: blur(var(--glas-blur));
border: 1px solid var(--glas-kante-2);
box-shadow: var(--sh-2), inset 0 1px 0 var(--glas-kante);
padding: 4px 12px 10px;
padding: 4px 12px 12px;
}
/* Kopf und Reiter sind innerhalb der Karte keine eigenen Flächen mehr. */
html.glas .plan-kopf .plan-header,
@@ -265,19 +267,25 @@ html.glas .modal-bg {
}
/* Inhalt bündig unter der Kopfkarte und im selben 12px-Abstand. */
html.glas .plan-page .plan-main-content { padding-left: 12px; padding-right: 12px; padding-top: 12px; }
html.glas .plan-kopf .plan-header { padding: 10px 12px 6px; }
html.glas .plan-kopf .plan-header { padding: 12px 12px 8px; }
html.glas .plan-kopf .plan-nav { padding: 0; gap: 2px; }
/* Reiter wie die Einträge der Seitenleiste (html.glas .nav-item.active):
32 px, helle Fläche mit Haarlinie, Schrift dunkel statt Akzentblau. */
html.glas .plan-kopf .plan-nav-item {
border-bottom: 0;
border-radius: 999px;
padding: 8px 16px;
font-size: 13.5px;
border-radius: 8px;
height: 32px;
padding: 0 12px;
font-size: 13px;
}
html.glas .plan-kopf .plan-nav-item:hover { background: var(--surface-sunk); }
/* Der Kopf ist selbst eine helle Karte — eine weiße Kapsel wie in der
(grau durchscheinenden) Seitenleiste verschwände darauf. Deshalb hier
die umgekehrte Stufe: etwas dunkler als die Karte, mit Haarlinie. */
html.glas .plan-kopf .plan-nav-item.is-active {
background: var(--glas-dicht);
color: var(--accent);
box-shadow: inset 0 1px 0 var(--glas-kante), 0 2px 10px rgba(21,58,86,.10);
background: rgba(16,24,32,.07);
color: var(--text);
box-shadow: inset 0 0 0 1px rgba(16,24,32,.08);
}
}
@media (min-width: 769px) and (prefers-color-scheme: dark) {
@@ -293,13 +301,13 @@ html.glas .modal-bg {
Grau, auf allen anderen Seiten den Glas-Hintergrund. */
html.glas .plan-main { background: transparent; }
html.glas .nav-item { border-radius: 999px; }
html.glas .nav-item { border-radius: 8px; }
html.glas .nav-item:hover,
html.glas .nav-item:visited:hover { background: var(--surface-sunk); }
html.glas .nav-item.active {
background: var(--glas-dicht);
color: var(--text);
box-shadow: inset 0 1px 0 var(--glas-kante), 0 2px 10px rgba(21,58,86,.10);
box-shadow: 0 0 0 1px var(--line-soft);
}
html.glas .nav-item.active .nav-icon { color: var(--accent); }
/* Die bestehenden Dunkel-Regeln für die Navigation setzen feste Werte —
@@ -373,9 +381,8 @@ html.glas .gantt-row.gantt-days .gantt-label { background: var(--gantt-label-gru
die (noch leere) App-Oberfläche, die nicht durchscheinen darf. */
html.glas .login-seite {
background:
radial-gradient(55% 50% at 8% 4%, color-mix(in srgb, var(--glas-hell) 70%, transparent), transparent 70%),
radial-gradient(45% 45% at 92% 12%, color-mix(in srgb, var(--glas-tief) 26%, transparent), transparent 70%),
radial-gradient(50% 55% at 72% 96%, color-mix(in srgb, #7fc3b6 26%, transparent), transparent 70%),
radial-gradient(60% 55% at 8% 4%, color-mix(in srgb, var(--glas-hell) 26%, transparent), transparent 70%),
radial-gradient(50% 50% at 92% 12%, color-mix(in srgb, var(--glas-tief) 9%, transparent), transparent 70%),
var(--bg);
}
+7
View File
@@ -76,6 +76,13 @@ function adminNavAbgleichen(){
const item = document.querySelector('#adminSubnav [data-admin-section="' + sec.key + '"]');
if(item) item.hidden = !adminAbschnittErlaubt(sec);
});
// Zwischenüberschriften (Personen/Vorgaben/System) nur, wenn darunter
// mindestens ein erlaubter Punkt steht.
document.querySelectorAll('#adminSubnav .sidebar-subgruppe').forEach(kopf => {
let el = kopf.nextElementSibling, sichtbar = false;
while(el && !el.classList.contains('sidebar-subgruppe')){ if(!el.hidden) sichtbar = true; el = el.nextElementSibling; }
kopf.hidden = !sichtbar;
});
}
// adminAbschnittErlaubt: darf das Konto diesen Abschnitt sehen?
+137 -63
View File
@@ -55,8 +55,6 @@ const ABSENCE_TYPE_CLASS = {
// renderDashboard()-Aufruf neu gestartet (vorheriges Intervall zuerst
// gestoppt), damit beim wiederholten Tab-Wechsel nicht mehrere Intervalle
// parallel laufen.
let dashboardClockTimer = null;
// renderDashboard: lädt GET /my-dashboard und rendert Begrüßung/Uhr,
// Kennzahlen sowie die drei Bereiche in #dashboardContent. Wird nur beim
// Wechsel auf den Tab aufgerufen (showTab()), nicht beim initialen
@@ -66,6 +64,9 @@ async function renderDashboard(){
if(!container) return;
container.innerHTML = '<div class="hint">Lade Dashboard…</div>';
// Notizen je Projekt (Anzahl) parallel — fällt sie aus, fehlt nur die Angabe.
const notizenAnfrage = fetch(baseUrl() + '/projekt-notizen/anzahl')
.then(r => r.ok ? r.json() : null).catch(() => null);
let data;
try {
const res = await fetch(baseUrl() + '/my-dashboard');
@@ -76,6 +77,8 @@ async function renderDashboard(){
return;
}
const nz = await notizenAnfrage;
dashNotizen = (nz && nz.ok && nz.anzahl) || {};
const weekLabel = data.weekLabel || null;
const tasks = Array.isArray(data.tasks) ? data.tasks : [];
const projectDates = Array.isArray(data.projectDates) ? data.projectDates : [];
@@ -103,12 +106,15 @@ async function renderDashboard(){
const heuteFaellig = tasks.filter(t => t.end === heute).length
+ projectDates.filter(d => d.heute).length;
const daten = dashDaten();
const sonntag = formatDateISO(new Date(dashWochenMontag().getTime() + 6 * 86400000));
const dieseWoche = daten.eintraege.filter(e => e.datum >= heute && e.datum <= sonntag).length;
let html = '';
html += renderDashboardKopf(weekLabel);
html += renderDashboardBand({
ueberfaellig, heuteFaellig,
ueberfaellig, heuteFaellig, dieseWoche,
aufgaben: tasks.length,
termine: projectDates.length,
freigaben: pending.length,
abwesend: absencesToday,
});
@@ -120,10 +126,10 @@ async function renderDashboard(){
// Neu zugewiesene Aufgaben (js/benachrichtigungen.js) — vor allem anderen,
// weil es das ist, was sich seit dem letzten Blick geändert hat.
html += '<div id="dashNeu"></div>';
html += renderDashWoche(daten);
html += '<div class="dash-zonen">';
// Linke Zone: die eigenen Projekte mit ihren Terminen
const daten = dashDaten();
html += '<div>';
const zuAlle = dashZuLesen();
const alleOffen = daten.projekte.some(x => !zuAlle.has(dashProjKey(x.p)));
@@ -153,7 +159,6 @@ async function renderDashboard(){
if(typeof renderDashNeu === 'function'){ renderDashNeu(); ladeBenachrichtigungen(true); }
dashKalDaten = daten;
renderDashKalender();
startDashboardClock();
}
// ---------- Meine Projekte + Kalender (Entwurf B "Monatsblatt") ----------
@@ -164,6 +169,8 @@ async function renderDashboard(){
let dashKalDaten = null;
let dashKalMonat = null; // { jahr, monat } (monat 0–11)
let dashKalTag = null; // ISO-Datum
// dashNotizen: { project_uid: {anzahl, zuletzt} } aus GET /projekt-notizen/anzahl.
let dashNotizen = {};
// dashDaten: sammelt aus der aktuellen Sitzung (currentWeek()) die Projekte
// des angemeldeten Kontos und alle Kalendereinträge. Ein Projekt gehört dazu,
@@ -440,9 +447,8 @@ function renderDashProjekte(daten){
const key = dashProjKey(p);
const istZu = zu.has(key);
const rollen = x.rollen.length
? x.rollen.map(r => '<span class="dash-rolle' + (r.state === 2 ? ' ist-neben' : '') + '" title="'
+ (r.state === 2 ? 'Unterstützung' : 'Hauptverantwortung') + '">' + esc(r.rolle) + '</span>').join('')
: '<span class="dash-rolle ist-neben" title="Nicht zugeordnet, aber mit eigener Aufgabe">nur Aufgabe</span>';
? x.rollen.map(r => esc(r.rolle) + (r.state === 2 ? ', Unterstützung' : '')).join(' / ')
: 'nur eigene Aufgabe';
const aktLp = (typeof lpActiveList === 'function' ? lpActiveList(p) : [])[0];
const pct = aktLp && p.lph && p.lph[aktLp] !== undefined && p.lph[aktLp] !== '' ? Math.max(0, Math.min(100, Number(p.lph[aktLp]) || 0)) : null;
const phase = aktLp
@@ -463,7 +469,7 @@ function renderDashProjekte(daten){
// Eingeklappt bleibt sichtbar, was Aufmerksamkeit braucht: wie viele
// Termine überfällig bzw. offen sind.
const kurz = (ueber.length ? '<span class="dash-proj-zahl ist-rot">' + ueber.length + ' überfällig</span>' : '')
+ '<span class="dash-proj-zahl">' + kommend.length + ' offen</span>';
+ '<span class="dash-proj-zahl' + (kommend.length ? ' ist-offen' : '') + '">' + kommend.length + ' offen</span>';
return '<div class="dash-karte dash-proj' + (istZu ? ' ist-zu' : '') + '" data-key="' + esc(key) + '">'
+ '<div class="dash-proj-kopf">'
+ (daten.projekte.length > 1 ? '<span class="dash-proj-griff" tabindex="0" role="button" title="Ziehen, um die Reihenfolge zu ändern (oder Pfeiltasten)" aria-label="Reihenfolge ändern" onmousedown="dashGriffDruck(this)" ontouchstart="dashGriffDruck(this)" onkeydown="dashGriffTaste(event, this)">' + icon('grip') + '</span>' : '')
@@ -472,10 +478,10 @@ function renderDashProjekte(daten){
+ '<span class="dash-proj-nr">' + esc(p.nr || '') + '</span><span class="dash-proj-titel">' + esc(p.name || '') + '</span>'
+ '</button>'
+ '<span class="dash-proj-kurz">' + kurz + '</span>'
+ '<span class="dash-rollen">' + rollen + '</span>'
+ '<a class="dash-proj-oeffnen" href="' + dashProjektUrl(p, 'dashboard') + '" onclick="return dashImTabOeffnen(event, this)" title="Projekt öffnen">Öffnen</a>'
+ '</div>'
+ '<div class="dash-proj-inhalt">'
+ renderDashProjektInfo(x, rollen)
+ '<div class="dash-proj-sub">' + phase + '</div>'
+ '<div class="dash-proj-termine">' + zeilen + mehr + '</div>'
+ '</div>'
@@ -483,6 +489,103 @@ function renderDashProjekte(daten){
}).join('') + '</div>';
}
// renderDashWoche: Band „Diese Woche“ (Entwurf „Bauplan“): Mo bis Fr als
// Spalten mit den Terminen der eigenen Projekte an diesem Tag — eigene
// Aufgaben, Aufgaben im Projekt, Meilensteine, Überfälliges. Je Tag höchstens
// DASH_WOCHE_JE_TAG, der Rest als „+ N weitere“. Parameter: daten (dashDaten()).
const DASH_WOCHE_JE_TAG = 4;
function renderDashWoche(daten){
if(!daten.geladen || !daten.projekte.length) return '';
const heute = heuteIso();
const mo = dashWochenMontag();
const TAGE = ['Mo', 'Di', 'Mi', 'Do', 'Fr'];
let spalten = '';
for(let i = 0; i < 5; i++){
const d = new Date(mo); d.setDate(mo.getDate() + i);
const iso = formatDateISO(d);
const e = daten.eintraege.filter(x => x.datum === iso)
.sort((a, b) => (b.meins ? 1 : 0) - (a.meins ? 1 : 0));
const sicht = e.slice(0, DASH_WOCHE_JE_TAG);
const rest = e.length - sicht.length;
spalten += '<div class="dash-woche-tag' + (iso === heute ? ' ist-heute' : '') + '">'
+ '<div class="dash-woche-kopf"><span>' + TAGE[i] + '</span><span class="dash-woche-datum">' + d.getDate() + '.' + (d.getMonth() + 1) + '.</span></div>'
+ '<div class="dash-woche-liste">'
+ (sicht.map(x => {
const art = x.art === 'meilenstein' ? 'ist-ms' : (x.datum < heute ? 'ist-ueber' : (x.meins ? 'ist-meins' : 'ist-fremd'));
return '<a class="dash-woche-eintrag ' + art + '" href="' + dashProjektUrl(x.projekt, x.art === 'meilenstein' ? 'terminplan' : 'aufgaben') + '" onclick="return dashImTabOeffnen(event, this)"'
+ ' title="' + esc(x.titel + ' (' + [x.projekt.nr, x.projekt.name].filter(Boolean).join(' ') + ')') + '">'
+ '<span class="dash-woche-titel">' + esc(x.titel) + '</span>'
+ '<span class="dash-woche-projekt">' + esc([x.projekt.nr, x.projekt.name].filter(Boolean).join(' ')) + '</span></a>';
}).join('') || '<span class="dash-woche-leer">keine Termine</span>')
+ (rest > 0 ? '<span class="dash-woche-mehr">+ ' + rest + ' weitere</span>' : '')
+ '</div></div>';
}
const legende = '<div class="dash-woche-legende">'
+ '<span><i class="ist-meins"></i>meine Aufgabe</span><span><i class="ist-fremd"></i>Aufgabe im Projekt</span>'
+ '<span><i class="ist-ms"></i>Meilenstein</span><span><i class="ist-ueber"></i>überfällig</span></div>';
return dashAbschnitt('Diese Woche', null, 'eigene Aufgaben und Termine meiner Projekte',
'<div class="dash-karte dash-woche"><div class="dash-woche-rahmen"><div class="dash-woche-raster">' + spalten + '</div></div>' + legende + '</div>');
}
// renderDashProjektInfo: Zeile „Meine Rolle | Team | Notizen“ im Projektblock
// (Eriks Wunsch): die eigene Rolle, die übrigen Beteiligten als Kürzel-Kreise
// in den Rollenfarben (wie Projektliste, beteiligtenAvatarHtml()) und — nur
// wenn vorhanden — die Zahl der Notizen mit Sprung in den Reiter „Notizen“.
// Parameter: x (Eintrag aus dashDaten()), rollenText. Rückgabe: HTML-String.
const DASH_TEAM_MAX = 5;
function renderDashProjektInfo(x, rollenText){
const p = x.p;
const w = currentWeek();
const ich = currentUser && currentUser.personId;
const personen = {};
((w && w.people) || []).forEach(q => { personen[q.id] = q; });
const eintraege = [];
Object.keys(p.assign || {}).forEach(key => {
const [pid, role] = key.split('|');
const state = Number(p.assign[key]) || 0;
if(state > 0) eintraege.push({ person: personen[pid] || { id: pid, name: pid, fullName: pid }, role, state });
});
const alle = beteiligteJePerson(eintraege);
const ichB = alle.find(b => b.person.id === ich);
const team = alle.filter(b => b.person.id !== ich);
const kreis = b => beteiligtenAvatarHtml(b, typeof roleClass === 'function' ? roleClass(b.role) : '');
const kurzName = b => {
const teile = String(b.person.fullName || b.person.name || '').trim().split(/\s+/);
return teile.length > 1 ? teile[0] + ' ' + teile[teile.length - 1][0] + '.' : teile[0];
};
const namen = team.length > 2
? team.slice(0, 2).map(kurzName).join(', ') + ' +' + (team.length - 2)
: team.map(kurzName).join(', ');
let html = '<div class="dash-proj-info">'
+ '<div class="dash-proj-feld"><span class="dash-proj-feld-titel">Meine Rolle</span>'
+ '<span class="dash-proj-feld-wert">' + (ichB ? kreis(ichB) : '') + '<span>' + rollenText + '</span></span></div>'
+ '<div class="dash-proj-feld"><span class="dash-proj-feld-titel">Team</span>'
+ '<span class="dash-proj-feld-wert">'
+ (team.length
? '<span class="dash-proj-team">' + team.slice(0, DASH_TEAM_MAX).map(kreis).join('') + '</span><span class="dash-proj-namen" title="' + esc(team.map(b => b.title).join('\n')) + '">' + esc(namen) + '</span>'
: '<span class="dash-proj-namen">niemand weiter</span>')
+ '</span></div>';
const n = dashNotizen[p.projectUid];
if(n && n.anzahl > 0){
html += '<a class="dash-proj-feld dash-proj-notizen" href="' + dashProjektUrl(p, 'notizen') + '" onclick="return dashImTabOeffnen(event, this)">'
+ '<span class="dash-proj-feld-titel">Notizen</span>'
+ '<span class="dash-proj-feld-wert">' + icon('fileText') + '<span><b>' + n.anzahl + '</b> ' + (n.anzahl === 1 ? 'Notiz' : 'Notizen') + '</span>'
+ (n.zuletzt ? '<span class="dash-proj-zuletzt">' + esc(dashZuletzt(n.zuletzt)) + '</span>' : '') + '</span></a>';
}
return html + '</div>';
}
// dashZuletzt: „zuletzt heute/gestern“ oder „vom 28.10.“ zu einem Zeitstempel.
function dashZuletzt(zeit){
const d = new Date(zeit);
if(isNaN(d)) return '';
const tage = -tageBis(formatDateISO(d));
if(tage <= 0) return 'zuletzt heute';
if(tage === 1) return 'zuletzt gestern';
return 'vom ' + d.getDate() + '.' + (d.getMonth() + 1) + '.';
}
// renderDashTermin: eine Terminzeile im Projektblock und in der Tagesliste
// des Kalenders. Eigene Aufgaben tragen das Zeichen "A" in Akzentfarbe,
// fremde Aufgaben dazu die Zuständigen, Meilensteine eine Raute.
@@ -593,22 +696,30 @@ function dashKalBlaettern(n){
renderDashKalender();
}
// renderDashboardKopf: Begrüßung und Datum links, Sitzung und Uhr klein
// rechts. Die Uhrzeit war zuvor die größte Zahl auf dem Bildschirm — sie ist
// die Angabe, nach der man am wenigsten handelt, und steht deshalb am Rand.
// Die Inhalte füllt startDashboardClock() live nach.
// Parameter: weekLabel. Rückgabe: HTML-String.
// renderDashboardKopf: Titel und Kalenderwoche statt Begrüßung (Entwurf
// „Bauplan“, Eriks Wahl): „KW 45, Mo 2. bis Fr 6. November 2026“, rechts die
// Sitzung. Parameter: weekLabel. Rückgabe: HTML-String.
function renderDashboardKopf(weekLabel){
const MONATE = ['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'];
const mo = dashWochenMontag();
const fr = new Date(mo); fr.setDate(mo.getDate() + 4);
const zeile = 'KW ' + getWeekNumber(mo) + ', Mo ' + mo.getDate() + '.'
+ (mo.getMonth() !== fr.getMonth() ? ' ' + MONATE[mo.getMonth()] : '')
+ ' bis Fr ' + fr.getDate() + '. ' + MONATE[fr.getMonth()] + ' ' + fr.getFullYear();
return '<div class="dash-kopf">'
+ '<p class="dash-gruss"><span id="dashboardGreetingText"></span>'
+ '<em id="dashboardGreetingDate"></em></p>'
+ '<div class="dash-kopf-rechts">'
+ (weekLabel ? '<span class="dash-sitzung">' + esc(weekLabel) + '</span>' : '')
+ '<span class="dash-uhr" id="dashboardClock"></span>'
+ '</div>'
+ '<div class="dash-kopf-links"><span class="dash-kopf-kw">' + esc(zeile) + '</span>'
+ '<h2 class="dash-titel">Meine Aufgaben</h2></div>'
+ (weekLabel ? '<div class="dash-kopf-rechts"><span class="dash-kopf-label">Sitzung</span><span class="dash-sitzung">' + esc(weekLabel) + '</span></div>' : '')
+ '</div>';
}
// dashWochenMontag: Montag der laufenden Woche (lokale Zeit, 0 Uhr).
function dashWochenMontag(){
const d = parseDateISO(heuteIso());
d.setDate(d.getDate() - ((d.getDay() + 6) % 7));
return d;
}
// renderDashboardBand: der Befund des Tages als Satz, daneben die Kennzahlen.
//
// Zuvor standen hier vier gleich große Kacheln, von denen meist drei eine
@@ -634,17 +745,17 @@ function renderDashboardBand(z){
satz = '<b class="ist-ruhig">Nichts offen, nichts überfällig.</b>';
}
const zahl = (wert, label) =>
'<div class="dash-zahl' + (wert === 0 ? ' ist-null' : '') + '"><b>' + wert + '</b><span>' + label + '</span></div>';
const zahl = (wert, label, rot) =>
'<div class="dash-zahl' + (wert === 0 ? ' ist-null' : '') + (rot ? ' ist-rot' : '') + '"><span>' + label + '</span><b>' + wert + '</b></div>';
const ruhig = z.ueberfaellig === 0 && z.heuteFaellig === 0;
return '<div class="dash-band' + (ruhig ? ' ist-ruhig' : '') + '">'
+ '<p class="dash-befund">' + satz + '</p>'
+ '<div class="dash-band-zahlen">'
+ zahl(z.aufgaben, 'offene Aufgaben')
+ zahl(z.termine, 'Termine im Blick')
+ zahl(z.ueberfaellig, 'überfällig', z.ueberfaellig > 0)
+ zahl(z.dieseWoche, 'diese Woche fällig')
+ zahl(z.freigaben, 'Freigaben')
+ zahl(z.abwesend, 'heute abwesend')
+ '</div></div>';
}
@@ -666,43 +777,6 @@ function dashLeer(text){
return '<div class="dash-karte"><div class="dash-leer">' + esc(text) + '</div></div>';
}
// greetingForHour:// greetingForHour: liefert je Tageszeit eine passende Begrüßung. Parameter:
// hour (Number, 0-23). Rückgabe: String.
function greetingForHour(hour){
if(hour < 5) return 'Gute Nacht';
if(hour < 11) return 'Guten Morgen';
if(hour < 18) return 'Guten Tag';
if(hour < 22) return 'Guten Abend';
return 'Gute Nacht';
}
// startDashboardClock: (re-)startet das Intervall, das Begrüßung, Datum und
// Uhrzeit im Dashboard-Header aktualisiert. Bricht sich selbst ab, sobald die
// Zielelemente nicht mehr im DOM sind (z. B. weil das Dashboard neu gerendert
// wurde). Keine Parameter, keine Rückgabe.
function startDashboardClock(){
if(dashboardClockTimer) clearInterval(dashboardClockTimer);
const name = (currentUser && (currentUser.displayName || currentUser.username)) || '';
const tick = () => {
const textEl = document.getElementById('dashboardGreetingText');
const dateEl = document.getElementById('dashboardGreetingDate');
const clockEl = document.getElementById('dashboardClock');
if(!textEl || !dateEl || !clockEl){
clearInterval(dashboardClockTimer);
dashboardClockTimer = null;
return;
}
const now = new Date();
// Begrüßung und Datum stehen in EINER Zeile (früher untereinander), das
// Datum als gedämpfter Zusatz — deshalb der führende Gedankenstrich.
textEl.textContent = greetingForHour(now.getHours()) + (name ? ', ' + name : '');
dateEl.textContent = ' — ' + now.toLocaleDateString('de-DE', { weekday: 'long', day: '2-digit', month: 'long', year: 'numeric' });
clockEl.textContent = now.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
};
tick();
dashboardClockTimer = setInterval(tick, 30000);
}
// relativerTag: "heute", "morgen", "in 5 Tagen", "seit 3 Tagen" zu einem
// ISO-Datum. Die Angabe steht neben dem Datum, weil "12.08." allein nicht
// sagt, ob das morgen oder in drei Wochen ist.
+1 -1
View File
@@ -44,7 +44,7 @@ const EINFUEHRUNG_SCHRITTE = [
text: () => 'Dein Konto: Passwort, E-Mail-Adresse, deine KI-Schlüssel und die Darstellung. Hier kannst du diesen <b>Rundgang jederzeit neu starten</b>.' },
{ id: 'admin', wenn: () => typeof hatAdminRecht === 'function' && hatAdminRecht(), ziel: '#adminAccordionToggle', titel: 'Admin',
text: () => 'Du darfst Teile der Verwaltung pflegen: ' + esc(typeof ADMIN_SECTIONS !== 'undefined' ? ADMIN_SECTIONS.filter(adminAbschnittErlaubt).map(s => s.title).join(', ') : '') + '.' },
{ id: 'ende', tab: 'dashboard', titel: 'Fertig!',
{ id: 'ende', tab: 'dashboard', titel: 'Rundgang abgeschlossen',
text: () => {
const hinweise = [];
if(currentUser && !currentUser.personId){
+1
View File
@@ -147,6 +147,7 @@ const TAB_SLUGS = {
wochenbericht: 'Wochenbericht',
load: 'Auslastung',
abwesenheit: 'Abwesenheiten',
notizen: 'Notizen',
people: 'Projektbeteiligte',
planung: 'Planung',
planchecker: 'PlanChecker',
+408
View File
@@ -0,0 +1,408 @@
// -----------------------------
// Seitenleiste „Notizen“ (Tab `notizen`, /Notizen)
// -----------------------------
// Eriks Wunsch: Jedes Konto sieht hier die Notizen ALLER seiner Projekte und
// legt auch projektunabhängige Notizen an, die sich beim Bearbeiten einem
// Projekt zuordnen lassen. Gleicher Aufbau wie der Reiter „Notizen“ im
// Projektbereich (js/plan/plan-notizen.js: Liste + Editor, Werkzeugleiste,
// automatisches Speichern), dazu ein Projektfilter und im Editor die Auswahl
// „Projekt“. Eigene Element-ids (mn…), weil der Projekt-Reiter gleichzeitig
// im DOM liegt.
//
// Daten: GET /meine-notizen (Notizen meiner Projekte + meine
// projektunabhängigen, project_uid = ''), sonst dieselben Routen
// /projekt-notizen* wie der Projekt-Reiter; PUT nimmt `uid` zum Zuordnen.
// Projektunabhängige Notizen sieht und ändert nur, wer sie angelegt hat.
(function(){
const SPEICHER_PAUSE_MS = 1200;
const OHNE = '__ohne__';
let liste = [];
let meineProjekte = new Set();
let geladen = false;
let ladeFehler = '';
let filter = ''; // '' = alle, OHNE = ohne Projekt, sonst uid
let suche = '';
let aktivId = null;
let basis = '';
let ungespeichert = false;
let speicherTimer = null;
let speichertGerade = false;
let beobachter = null;
let loeschenFragt = null;
let eigenesSchreiben = 0; // Zeitpunkt der letzten eigenen Änderung (Live-Echo auslassen)
const el = () => document.getElementById('notizen');
const aktiv = () => liste.find(n => n.id === aktivId) || null;
const darf = n => !n || !n.uid ? true : (typeof hatRecht === 'function' && hatRecht('proj.plan'));
const darfProjekte = () => typeof hatRecht === 'function' && hatRecht('proj.plan');
// Projekte der aktuellen Sitzung je projectUid (Name und Nummer).
function projekte(){
const w = typeof currentWeek === 'function' ? currentWeek() : null;
const m = new Map();
((w && w.projects) || []).forEach(p => { if(p.projectUid) m.set(String(p.projectUid), p); });
return m;
}
function projektName(uid, karte){
if(!uid) return 'Ohne Projekt';
const p = (karte || projekte()).get(uid);
return p ? [p.nr, p.name].filter(Boolean).join(' ') : 'Projekt nicht in der aktuellen Sitzung';
}
function zeit(iso){
if(!iso) return '';
const d = new Date(iso);
if(isNaN(d)) return '';
const heute = new Date();
const gestern = new Date(heute); gestern.setDate(heute.getDate() - 1);
const uhr = d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
if(d.toDateString() === heute.toDateString()) return 'heute ' + uhr;
if(d.toDateString() === gestern.toDateString()) return 'gestern ' + uhr;
return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: d.getFullYear() === heute.getFullYear() ? undefined : 'numeric' });
}
const klartext = html => typeof statusPlainText === 'function' ? statusPlainText(html) : String(html || '').replace(/<[^>]+>/g, ' ');
function sortieren(){
liste.sort((a, b) => (b.angeheftet - a.angeheftet) || String(b.geaendertAm).localeCompare(String(a.geaendertAm)));
}
// ---------- Laden ----------
function laden(){
ladeFehler = '';
return fetch(baseUrl() + '/meine-notizen')
.then(r => r.json().then(d => ({ ok: r.ok, d })))
.then(({ ok, d }) => {
if(!ok || !d.ok) throw new Error(d.error || 'Laden fehlgeschlagen');
liste = d.notizen || [];
meineProjekte = new Set(d.meineProjekte || []);
sortieren();
geladen = true;
})
.catch(e => { ladeFehler = e.message || 'Die Notizen konnten nicht geladen werden.'; liste = []; geladen = true; });
}
// ---------- Darstellung ----------
window.renderMeineNotizen = function(){
const box = el();
if(!box) return;
box.innerHTML = '<div class="notizen-laedt">Notizen werden geladen…</div>';
jetztSpeichern();
laden().then(aufbauen);
};
function sichtbar(){
const s = suche.trim().toLowerCase();
return liste.filter(n => (!filter || (filter === OHNE ? !n.uid : n.uid === filter))
&& (!s || (n.titel + ' ' + klartext(n.html) + ' ' + projektName(n.uid)).toLowerCase().includes(s)));
}
function filterOptionen(){
const karte = projekte();
const mitNotizen = new Set(liste.filter(n => n.uid).map(n => n.uid));
const uids = Array.from(new Set([...meineProjekte, ...mitNotizen]))
.sort((a, b) => projektName(a, karte).localeCompare(projektName(b, karte), 'de'));
const zahl = f => liste.filter(n => f === OHNE ? !n.uid : n.uid === f).length;
return '<option value="">Alle Projekte (' + liste.length + ')</option>'
+ '<option value="' + OHNE + '"' + (filter === OHNE ? ' selected' : '') + '>Ohne Projekt (' + zahl(OHNE) + ')</option>'
+ uids.map(u => '<option value="' + esc(u) + '"' + (filter === u ? ' selected' : '') + '>' + esc(projektName(u, karte)) + ' (' + zahl(u) + ')</option>').join('');
}
function aufbauen(){
const box = el();
if(!box) return;
const treffer = sichtbar();
if(!treffer.some(n => n.id === aktivId)) aktivId = treffer.length ? treffer[0].id : null;
box.innerHTML = `
<div class="mn-seite">
<div class="mn-kopf">
<div class="mn-kopf-links">
<span class="mn-kopf-unter">Notizen aller meiner Projekte und eigene Notizen ohne Projekt</span>
<h2 class="mn-titel">Notizen</h2>
</div>
<label class="mn-filter">Projekt
<select id="mnFilter" onchange="mnFilter(this.value)">${filterOptionen()}</select>
</label>
</div>
<div class="notizen mn-notizen">
<aside class="notizen-liste">
<div class="notizen-suchzeile">
<input type="search" id="mnSuche" class="notizen-suche" placeholder="Notizen durchsuchen…"
aria-label="Notizen durchsuchen" value="${esc(suche)}" oninput="mnSuchen(this.value)">
<button type="button" class="notizen-neu" onclick="mnNeu()">+ Neu</button>
</div>
<div id="mnEintraege" class="notizen-eintraege"></div>
</aside>
<div class="notizen-editor" id="mnEditor"></div>
</div>
</div>`;
listeZeichnen();
editorZeichnen();
}
function listeZeichnen(){
const box = document.getElementById('mnEintraege');
if(!box) return;
if(ladeFehler){ box.innerHTML = '<div class="notizen-leer ist-fehler">' + esc(ladeFehler) + '</div>'; return; }
const treffer = sichtbar();
if(!liste.length){ box.innerHTML = '<div class="notizen-leer">Noch keine Notizen. Mit „+ Neu“ eine eigene anlegen.</div>'; return; }
if(!treffer.length){ box.innerHTML = '<div class="notizen-leer">' + (suche ? 'Keine Notiz enthält „' + esc(suche) + '“.' : 'Keine Notizen in dieser Auswahl.') + '</div>'; return; }
const karte = projekte();
const zeile = n => {
const vorschau = klartext(n.html).replace(/\s+/g, ' ').trim().slice(0, 90);
return `<button type="button" class="notizen-eintrag${n.id === aktivId ? ' ist-aktiv' : ''}" onclick="mnWaehlen('${esc(n.id)}')">
<span class="notizen-eintrag-titel">${esc(n.titel || 'Ohne Titel')}${n.quelle === 'mail' ? ' <span class="notizen-chip">Mail</span>' : ''}</span>
<span class="notizen-eintrag-text">${esc(vorschau || 'Kein Text')}</span>
<span class="notizen-eintrag-meta"><span class="mn-projekt${n.uid ? '' : ' ist-ohne'}">${esc(projektName(n.uid, karte))}</span> · ${esc(zeit(n.geaendertAm))}</span>
</button>`;
};
const oben = treffer.filter(n => n.angeheftet), rest = treffer.filter(n => !n.angeheftet);
box.innerHTML = (oben.length ? '<div class="notizen-gruppe"><span class="notizen-pin-punkt"></span>Angeheftet</div>' + oben.map(zeile).join('') : '')
+ (rest.length ? (oben.length ? '<div class="notizen-gruppe">Weitere</div>' : '') + rest.map(zeile).join('') : '');
}
// projektAuswahl: Auswahl im Editor — „Ohne Projekt“, meine Projekte, dann
// die übrigen der Sitzung. Ohne Recht proj.plan nur „Ohne Projekt“ (und das
// aktuelle Projekt, damit der Stand lesbar bleibt).
function projektAuswahl(n){
const karte = projekte();
const name = u => projektName(u, karte);
const sort = arr => arr.sort((a, b) => name(a).localeCompare(name(b), 'de'));
const opt = u => '<option value="' + esc(u) + '"' + (n.uid === u ? ' selected' : '') + '>' + esc(name(u)) + '</option>';
const meine = sort(Array.from(meineProjekte).filter(u => karte.has(u)));
const weitere = sort(Array.from(karte.keys()).filter(u => !meineProjekte.has(u)));
const sperren = !darf(n) || (!darfProjekte() && !n.uid);
let html = '<option value=""' + (!n.uid ? ' selected' : '') + '>Ohne Projekt</option>';
if(darfProjekte()){
if(meine.length) html += '<optgroup label="Meine Projekte">' + meine.map(opt).join('') + '</optgroup>';
if(weitere.length) html += '<optgroup label="Weitere Projekte">' + weitere.map(opt).join('') + '</optgroup>';
}
if(n.uid && !karte.has(n.uid)) html += opt(n.uid);
return '<label class="mn-zuordnung">Projekt <select id="mnProjekt" onchange="mnZuordnen(this.value)"'
+ (sperren ? ' disabled title="' + (darfProjekte() ? '' : 'Zum Zuordnen fehlt das Recht, den Terminplan zu bearbeiten.') + '"' : '') + '>' + html + '</select></label>';
}
function editorZeichnen(){
const box = document.getElementById('mnEditor');
if(!box) return;
if(beobachter){ beobachter.disconnect(); beobachter = null; }
const n = aktiv();
if(!n){
box.innerHTML = `<div class="notizen-editor-leer">
<b>${liste.length ? 'Notiz links auswählen' : 'Noch keine Notizen'}</b>
<span>Mit „+ Neu“ eine Notiz anlegen. Sie gehört zunächst nur Ihnen und lässt sich oben rechts einem Projekt zuordnen.</span>
</div>`;
return;
}
const schreiben = darf(n);
basis = n.geaendertAm;
ungespeichert = false;
const projekt = n.uid ? projekte().get(n.uid) : null;
box.innerHTML = `
<div class="notizen-editor-kopf">
<input type="text" id="mnTitel" class="notizen-titel" value="${esc(n.titel)}" placeholder="Titel der Notiz"
aria-label="Titel der Notiz" ${schreiben ? '' : 'readonly'} oninput="mnGeaendert()" onblur="mnJetztSpeichern()">
${schreiben ? `<button type="button" class="notizen-symbol${n.angeheftet ? ' ist-an' : ''}" onclick="mnAnheften()"
aria-pressed="${n.angeheftet}">${n.angeheftet ? 'Angeheftet' : 'Anheften'}</button>
<button type="button" class="notizen-symbol ist-gefahr" id="mnLoeschen" onclick="mnLoeschen()">Löschen</button>` : ''}
</div>
<div class="mn-editor-zeile">
${projektAuswahl(n)}
${projekt ? `<a class="mn-projekt-link" href="#" onclick="return mnProjektOeffnen('${esc(projekt.id)}')">Im Projekt öffnen</a>` : ''}
${!n.uid ? '<span class="mn-hinweis">Nur für Sie sichtbar, bis sie einem Projekt zugeordnet ist</span>' : ''}
</div>
<div class="richtext-wrap notizen-richtext">
${schreiben ? richtextToolbarHtml('#mnText') : ''}
${richtextFeldHtml('notizen-text', sanitizeStatusHtml(n.html || ''), 'Notiz schreiben…', 'id="mnText" onblur="mnJetztSpeichern()"', schreiben)}
</div>
<div class="notizen-fuss">
<span>Angelegt von ${esc(n.erstelltVon)} ${esc(zeit(n.erstelltAm))}${n.geaendertAm !== n.erstelltAm ? ' · zuletzt geändert von ' + esc(n.geaendertVon) + ' ' + esc(zeit(n.geaendertAm)) : ''}</span>
<span id="mnStatus" class="notizen-status"></span>
</div>`;
const text = document.getElementById('mnText');
if(text && schreiben){
beobachter = new MutationObserver(() => mnGeaendert());
beobachter.observe(text, { childList: true, subtree: true, characterData: true, attributes: true });
}
}
function status(text, art){
const s = document.getElementById('mnStatus');
if(!s) return;
s.className = 'notizen-status' + (art ? ' ist-' + art : '');
s.innerHTML = text;
}
function hinweis(text){ if(typeof zeigeHinweis === 'function') zeigeHinweis(text); else status(esc(text), 'fehler'); }
// ---------- Bedienung ----------
window.mnFilter = function(wert){ jetztSpeichern(); filter = wert || ''; aufbauen(); };
window.mnSuchen = function(wert){ suche = wert || ''; listeZeichnen(); };
window.mnWaehlen = function(id){
if(id === aktivId) return;
jetztSpeichern();
aktivId = id;
listeZeichnen();
editorZeichnen();
};
window.mnGeaendert = function(){
const n = aktiv();
if(!n || !darf(n)) return;
ungespeichert = true;
status('Ungespeichert');
clearTimeout(speicherTimer);
speicherTimer = setTimeout(jetztSpeichern, SPEICHER_PAUSE_MS);
};
window.mnJetztSpeichern = function(){ jetztSpeichern(); };
function senden(n, nutzlast){
eigenesSchreiben = Date.now();
return fetch(baseUrl() + '/projekt-notizen/' + encodeURIComponent(n.id), {
method: 'PUT', headers: { 'Content-Type': 'application/json; charset=utf-8' }, body: JSON.stringify(nutzlast)
}).then(r => r.json().then(d => ({ r, d })));
}
function jetztSpeichern(ohneBasis){
clearTimeout(speicherTimer);
const n = aktiv();
if(!n || !ungespeichert || !darf(n)) return Promise.resolve();
if(speichertGerade){ speicherTimer = setTimeout(jetztSpeichern, 400); return Promise.resolve(); }
const titelEl = document.getElementById('mnTitel');
const textEl = document.getElementById('mnText');
const titel = titelEl ? titelEl.value : n.titel;
const html = textEl ? textEl.innerHTML : n.html;
ungespeichert = false;
speichertGerade = true;
status('Speichert…');
return senden(n, { titel, html, basis: ohneBasis ? '' : basis }).then(({ r, d }) => {
if(r.status === 409 && d.konflikt){
ungespeichert = true;
status(esc(d.error) + ' <button type="button" class="notizen-link" onclick="mnKonflikt(true)">Meine Fassung speichern</button>'
+ ' <button type="button" class="notizen-link" onclick="mnKonflikt(false)">Neuen Stand laden</button>', 'warnung');
n._fremd = d.notiz;
return;
}
if(!r.ok || !d.ok) throw new Error(d.error || 'Speichern fehlgeschlagen');
Object.assign(n, d.notiz, { titel, html });
if(n.id === aktivId) basis = d.notiz.geaendertAm;
sortieren();
listeZeichnen();
if(!ungespeichert) status('Gespeichert ✓', 'ok');
}).catch(e => {
ungespeichert = true;
status('Nicht gespeichert: ' + esc(e.message) + ' — wird erneut versucht.', 'fehler');
speicherTimer = setTimeout(jetztSpeichern, 5000);
}).finally(() => { speichertGerade = false; });
}
window.mnKonflikt = function(meine){
const n = aktiv();
if(!n) return;
if(meine){ ungespeichert = true; jetztSpeichern(true); return; }
if(n._fremd) Object.assign(n, n._fremd);
delete n._fremd;
ungespeichert = false;
listeZeichnen();
editorZeichnen();
};
// mnNeu: neue Notiz — im gefilterten Projekt, sonst ohne Projekt.
window.mnNeu = function(){
jetztSpeichern();
const uid = filter && filter !== OHNE && darfProjekte() ? filter : '';
eigenesSchreiben = Date.now();
fetch(baseUrl() + '/projekt-notizen', {
method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8' },
body: JSON.stringify({ uid, titel: '', html: '' })
}).then(r => r.json()).then(d => {
if(!d.ok) throw new Error(d.error || 'Anlegen fehlgeschlagen');
liste = liste.filter(x => x.id !== d.notiz.id).concat([d.notiz]);
sortieren();
aktivId = d.notiz.id;
suche = '';
aufbauen();
const t = document.getElementById('mnTitel');
if(t) t.focus();
}).catch(e => hinweis('Notiz konnte nicht angelegt werden: ' + e.message));
};
// mnZuordnen: Notiz einem Projekt zuordnen (oder wieder lösen). Offene
// Änderungen werden vorher gespeichert.
window.mnZuordnen = function(uid){
const n = aktiv();
if(!n) return;
jetztSpeichern().then(() => senden(n, { uid: uid || '' })).then(({ r, d }) => {
if(!r.ok || !d.ok) throw new Error(d.error || 'Zuordnen fehlgeschlagen');
Object.assign(n, d.notiz);
if(n.uid) meineProjekte.add(n.uid);
if(filter && filter !== (n.uid || OHNE)) filter = n.uid || OHNE;
aufbauen();
status(n.uid ? 'Dem Projekt zugeordnet ✓' : 'Vom Projekt gelöst ✓', 'ok');
}).catch(e => { hinweis(e.message); editorZeichnen(); });
};
window.mnAnheften = function(){
const n = aktiv();
if(!n) return;
jetztSpeichern().then(() => senden(n, { angeheftet: !n.angeheftet })).then(({ d }) => {
if(!d.ok) throw new Error(d.error || 'Fehler');
n.angeheftet = d.notiz.angeheftet;
sortieren();
listeZeichnen();
editorZeichnen();
}).catch(e => hinweis('Anheften fehlgeschlagen: ' + e.message));
};
window.mnLoeschen = function(){
const n = aktiv();
const knopf = document.getElementById('mnLoeschen');
if(!n || !knopf) return;
if(!loeschenFragt){
knopf.textContent = 'Wirklich löschen?';
knopf.classList.add('ist-fragend');
loeschenFragt = setTimeout(() => {
loeschenFragt = null;
const k = document.getElementById('mnLoeschen');
if(k){ k.textContent = 'Löschen'; k.classList.remove('ist-fragend'); }
}, 4000);
return;
}
clearTimeout(loeschenFragt); loeschenFragt = null;
clearTimeout(speicherTimer); ungespeichert = false;
eigenesSchreiben = Date.now();
fetch(baseUrl() + '/projekt-notizen/' + encodeURIComponent(n.id), { method: 'DELETE' })
.then(r => r.json().then(d => ({ r, d }))).then(({ r, d }) => {
if(!r.ok || !d.ok) throw new Error(d.error || 'Löschen fehlgeschlagen');
liste = liste.filter(x => x.id !== n.id);
aktivId = null;
aufbauen();
}).catch(e => { status(esc(e.message), 'fehler'); const k = document.getElementById('mnLoeschen'); if(k){ k.textContent = 'Löschen'; k.classList.remove('ist-fragend'); } });
};
window.mnProjektOeffnen = function(pid){
jetztSpeichern();
location.hash = 'notizen';
if(typeof openProjectPlan === 'function') openProjectPlan(pid);
return false;
};
// ---------- Live: Änderungen aus anderen Browsern ----------
window.meineNotizenLive = function(uid){
if(typeof currentTab === 'undefined' || currentTab !== 'notizen' || !geladen) return;
if(uid && !meineProjekte.has(uid)) return;
const vorher = aktiv();
const tippt = document.activeElement && document.activeElement.closest && document.activeElement.closest('#mnEditor');
// Das Echo der eigenen Speicherung (kommt auch in diesem Tab an) auslassen.
if(ungespeichert || tippt || Date.now() - eigenesSchreiben < 3000) return;
laden().then(() => {
if(vorher && liste.some(x => x.id === vorher.id)) aktivId = vorher.id;
aufbauen();
});
};
// Klick irgendwo anders: die Rückfrage „Wirklich löschen?“ zurücknehmen.
document.addEventListener('pointerdown', e => {
if(!loeschenFragt || (e.target.closest && e.target.closest('#mnLoeschen'))) return;
clearTimeout(loeschenFragt); loeschenFragt = null;
const k = document.getElementById('mnLoeschen');
if(k){ k.textContent = 'Löschen'; k.classList.remove('ist-fragend'); }
}, true);
window.addEventListener('pagehide', () => { if(ungespeichert) jetztSpeichern(); });
})();
+3 -3
View File
@@ -540,7 +540,7 @@
const ampel = loadAmpelColor(l.quote);
const breite = Math.min(100, l.quote * 100);
const absCount = app.countPersonAbsences(l.person.id);
const absBadge = absCount > 0 ? `<div class="hint" style="font-size:10px;margin-top:2px">${absCount} Abwesenheit(en)</div>` : '';
const absBadge = absCount > 0 ? `<div class="hint" style="font-size:11px;margin-top:4px">${absCount} Abwesenheit(en)</div>` : '';
let todoListHtml;
if(l.todos.length === 0){
@@ -566,12 +566,12 @@
: `<div class="plan-resource-other muted">keine Aufgaben mit Aufwand</div>`;
html += `<div class="load-card plan-resource-card plan-resource-clickable" onclick="openAbsenceModal('${esc(l.person.id)}')" title="Klicken, um Urlaube/Abwesenheiten zu bearbeiten">
<h3>${esc(l.person.name)} <span style="font-size:10px;color:#888;font-weight:normal">(${esc(l.roles.join(' / '))})</span></h3>
<h3>${esc(l.person.name)} <span style="font-size:11px;color:var(--text-subtle);font-weight:400">(${esc(l.roles.join(' / '))})</span></h3>
<div class="load-num" style="font-size:18px;color:${ampel}">${stundenFormat(l.stunden)}<span style="font-size:11px;font-weight:400">h diese Woche</span></div>
<div class="bar" style="height:6px;margin-top:4px"><div style="width:${breite}%;background:${ampel}"></div></div>
${offenText}
${otherBadge}
<div class="load-detail" style="font-size:10px;margin-top:4px">${todoListHtml}</div>
<div class="load-detail" style="font-size:11px;margin-top:4px">${todoListHtml}</div>
${absBadge}
</div>`;
});
+7
View File
@@ -406,5 +406,12 @@
ausDaten(e.clipboardData);
});
// Beim Verlassen der Seite noch speichern, was offen ist.
// Klick irgendwo anders: die Rückfrage „Wirklich löschen?“ zurücknehmen.
document.addEventListener('pointerdown', e => {
if(!loeschenFragt || (e.target.closest && e.target.closest('#notizLoeschen'))) return;
clearTimeout(loeschenFragt); loeschenFragt = null;
const k = document.getElementById('notizLoeschen');
if(k){ k.textContent = 'Löschen'; k.classList.remove('ist-fragend'); }
}, true);
window.addEventListener('pagehide', () => { if(ungespeichert) jetztSpeichern(); });
})();
+1 -1
View File
@@ -100,7 +100,7 @@
// ein Fallback ist ein akzeptiertes Ergebnis, kein Fehlschlag.
function zeigeFallbackHinweis(msg){
const el = document.getElementById('protokollTodosFallback');
if(el){ el.textContent = '⚠️ ' + msg; el.hidden = false; }
if(el){ el.textContent = msg; el.hidden = false; }
}
function verbergeFallbackHinweis(){
const el = document.getElementById('protokollTodosFallback');
+2 -2
View File
@@ -548,7 +548,7 @@
// ab, verschwindet also nie von selbst, sondern nur wenn man das
// Protokoll erneut öffnet — bewusst so, die Herkunft bleibt sichtbar.
const importBanner = p.quelleDateiname
? `<div class="protokoll-import-banner">🔎 Punkte automatisch aus „${esc(p.quelleDateiname)}“ erkannt — bitte vor dem Speichern prüfen und bei Bedarf korrigieren.</div>`
? `<div class="protokoll-import-banner">Punkte automatisch aus „${esc(p.quelleDateiname)}“ erkannt — bitte vor dem Speichern prüfen und bei Bedarf korrigieren.</div>`
: '';
// Rein clientseitiges Merkmal (siehe importiereProtokoll() oben) — nur
// direkt nach GENAU dem Import gesetzt, der die KI angefordert, aber
@@ -559,7 +559,7 @@
// das war Eriks Rückmeldung, nachdem er von einem KI-Fehlschlag nichts
// in der Oberfläche sah.
const kiFallbackBanner = p._kiFallbackHinweis
? `<div class="protokoll-ki-fallback-banner">⚠️ Die KI-Erkennung ist fehlgeschlagen (${esc(p._kiFallbackHinweis)}) — die Punkte wurden stattdessen per Textmuster erkannt.</div>`
? `<div class="protokoll-ki-fallback-banner">Die KI-Erkennung ist fehlgeschlagen (${esc(p._kiFallbackHinweis)}) — die Punkte wurden stattdessen per Textmuster erkannt.</div>`
: '';
const kopf = `
${kiFallbackBanner}
+1 -1
View File
@@ -128,7 +128,7 @@
if(moved.size > 0){
app.savePlan();
renderPlanGantt();
app.showToast(moved.size + ' Element(e) automatisch verschoben.' + (hasCycle ? ' Achtung: zyklische Abhängigkeit gefunden!' : ''));
app.showToast(moved.size + ' Element(e) automatisch verschoben.' + (hasCycle ? ' Zyklische Abhängigkeit gefunden — bitte Vorgänger prüfen.' : ''));
} else if(hasCycle){
app.showToast('Achtung: zyklische Abhängigkeit gefunden – bitte Vorgänger prüfen.');
} else {
+5 -1
View File
@@ -541,7 +541,11 @@ function liveEreignis(d){
if(d.typ === 'woche') return liveWocheNachladen(d.label, d.geloescht);
if(d.typ === 'neu') return liveAllesNeu();
if(d.typ === 'personen') return personenNachladen();
if(d.typ === 'notizen' && typeof notizenLiveAenderung === 'function') return notizenLiveAenderung(d.uid);
if(d.typ === 'notizen'){
if(typeof meineNotizenLive === 'function') meineNotizenLive(d.uid);
if(typeof notizenLiveAenderung === 'function') notizenLiveAenderung(d.uid);
return;
}
}
// personenNachladen: Personenliste samt Abwesenheiten neu holen (nach einer
+6 -2
View File
@@ -23,7 +23,7 @@
// Bereich wie jeder andere: Menüband und Seitenleiste bleiben stehen, der
// Wechsel dorthin läuft über denselben showTab()-Weg wie der Wechsel vom
// Dashboard ins Projektboard.
const TAB_PANELS = ['board','wochenbericht','load','abwesenheit','people','planung','planchecker','dashboard','wand','settings','admin','plan'];
const TAB_PANELS = ['board','wochenbericht','load','abwesenheit','notizen','people','planung','planchecker','dashboard','wand','settings','admin','plan'];
function showTab(t){
// Weichen nach Rollen & Rechte (UI-Gate, die Routen dahinter prüft der
@@ -61,6 +61,7 @@ function showTab(t){
if(t==='wochenbericht') renderWochenbericht();
if(t==='load') renderLoad();
if(t==='abwesenheit'){ abwBeimOeffnen(); renderAbwesenheiten(); }
if(t==='notizen' && typeof renderMeineNotizen === 'function') renderMeineNotizen();
if(t==='people') renderPeople();
if(t==='planung') renderPlanung();
if(t==='planchecker') renderPlanChecker();
@@ -108,6 +109,9 @@ function syncPlanNavItem(){
const name = app && app.projName ? app.projName : '';
item.style.display = name ? '' : 'none';
const label = document.getElementById('planNavItemLabel');
if(label) label.textContent = name || 'Projektbereich';
// Projektnummer in Mono-Ziffern davor (Kennzeichen „Bauplan“).
if(label) label.innerHTML = name
? (app.projNr ? '<span class="nav-projnr">' + esc(app.projNr) + '</span> ' : '') + esc(name)
: 'Projektbereich';
item.title = name ? ('Projektbereich: ' + name) : 'Projektbereich';
}
+331 -319
View File
File diff suppressed because it is too large. Load diff
+868 -675
View File
File diff suppressed because it is too large. Load diff