diff --git a/.gitignore b/.gitignore index ec99bcc..9b901ee 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ sessions/*.db-shm # macOS-Artefakte .DS_Store +sessions/*.txt diff --git a/CLAUDE.md b/CLAUDE.md index 7248dbd..4318002 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,13 +9,56 @@ Single-page app for tracking engineering projects across weekly sessions step, no bundler, no tests. Data is persisted in a SQLite database (`node:sqlite`, needs Node >= 22.13) at `sessions/projektuebersicht.db`. -Two HTML entrypoints, both served statically by Express: -- `Test.html` — the main app (tabs: Projektboard, Auslastung, Projekte, - Projektbeteiligte). Served at `/`. -- `project-plan.html` — per-project Gantt/TODO planning view, opened in a new - tab via `openProjectPlan()` in `js/gantt.js`. Loads the shared helper modules - (`globals`, `utils`, `ui`, `date-picker`, `server`, `gantt`) plus the - plan modules under `js/plan/` — NOT the full main module set. +**ONE** HTML entrypoint, served statically by Express: +- `Test.html` — the whole app (sidebar nav items, in order: Meine Aufgaben, + **Projekte**, Wochenbericht, Auslastung, Projektbeteiligte, then + **Einstellungen** and — for admins only — **Admin**). Served at `/`. It + also contains the **Projektbereich** — the area of ONE project — as the + tab `plan` (`
`), with its own sub-navigation (`.plan-nav`) + over six sections: **Dashboard**, **Terminplan** (Gantt), **Aufgaben** + (task/milestone list), **Leistung** (erbrachter Leistungswert, see below), + **Dateien**, **Einstellungen** (project master data incl. honorar and the + beauftragte Leistungsphasen). Opened via `openProjectPlan()` in + `js/gantt.js`, which is now just `setPlanTarget()` + `showTab('plan')` — + the same kind of switch as Dashboard → Projekte, sidebar and topbar + stay put. + **There used to be a separate `Projekte` tab next to the Projektboard.** It + listed the very same projects a second time and offered a second editing + mask for the very same fields. Both are now ONE list — the tab `board`, + labelled **Projekte** (see "Eine Projektliste" below). The URL still reads `/Projektplan/?weekLabel=…&projNr=…` + and the selected section still lives in the URL fragment (`…#aufgaben`), + so both stay linkable. See "Projektbereich" below. +**`admin-users.html` no longer exists either.** It was a standalone +admin-only page with a hand-built header instead of the sidebar and its own +`checkAuthAndInit()` + role check. Its markup moved into `Test.html` as the +`benutzer` section of the Admin area, its ` - - -
-
-
-

Projektplan

-
-
-
- - -
-
- -
-
-
- - - - - - - - - - - - -
-
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/styles.css b/public/styles.css index 832c101..8024c81 100644 --- a/public/styles.css +++ b/public/styles.css @@ -20,6 +20,25 @@ --green: #248a3d; --amber: #b25000; --red: #d70015; + --red-hover: #e8112d; + --red-active: #b8000f; + + /* Flächen des Anwesenheits-Kacheln im Zustand "anwesend" — eigene Tokens, + weil eine feste rgba-Grünfläche im Dark Mode nicht trägt. */ + --attend-bg: rgba(36,138,61,.10); + --attend-bg-hover: rgba(36,138,61,.16); + + /* Kopfband der Wochenbericht-Karten je Änderungsart. Das Band trägt weiße + Schrift, deshalb etwas dunkler als --green (#248a3d käme nur auf + Kontrast 4,4 : 1 und bliebe damit unter den 4,5 : 1 für normalen Text). */ + --band-neu: #0071e3; + --band-fortschritt: #22823a; + + /* Gelbe Markierung neu hinzugekommener Protokoll-Punkte — bewusst eigenes + Token statt --band-neu (dort blau, bedeutet "neu kommentiertes Projekt" + im Wochenbericht — eine andere Bedeutung, die hier kollidieren würde). */ + --protokoll-neu-bg: #fff6db; + --protokoll-neu-border: #e6b800; --r-sm: 6px; --r-md: 8px; @@ -52,45 +71,75 @@ body { .container { padding: 20px 24px; } -/* === Kopfzeile & Navigation === */ -header { +/* === App-Shell: globales Menüband oben + linke Navigations-Sidebar + + Hauptbereich (OpenProject-Anordnung: ein durchgehendes globales Menüband + trägt Marke, aktuellen Kontext [Sitzung] und alle Aktionen/Konto rechts; + die Sidebar darunter ist reine Modul-Navigation, sonst nichts) === */ +.app-shell { display: flex; flex-direction: column; min-height: 100vh; } +.app-body { display: flex; flex: 1; min-height: 0; } +.app-main { flex: 1; min-width: 0; } + +/* Generisches Icon-System (js/icons.js): Icons skalieren wie ein Zeichen mit + der umgebenden Schriftgröße (1.1em), erben die Textfarbe über currentColor. */ +.icon { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; } +.icon svg { width: 1.1em; height: 1.1em; display: block; } + +/* === Globales Menüband (immer sichtbar, alle Breiten) === */ +.app-topbar { + position: sticky; + top: 0; + z-index: 140; + display: flex; + align-items: center; + flex-wrap: wrap; + row-gap: 8px; + gap: 12px; + padding: 8px 20px; background: rgba(255,255,255,.82); -webkit-backdrop-filter: saturate(180%) blur(20px); backdrop-filter: saturate(180%) blur(20px); border-bottom: 1px solid var(--line-soft); - color: var(--text); - padding: 10px 24px; } -.header-bar { - position: relative; - display: flex; - align-items: center; - gap: 16px; - min-height: 48px; -} +.mobile-nav-toggle { display: inline-flex; } +@media (min-width: 769px) { .app-topbar .mobile-nav-toggle { display: none; } } -header h1 { - font-size: 19px; +.topbar-brand { display: flex; align-items: center; gap: 8px; flex-shrink: 0; } +.topbar-logo { height: 28px; width: auto; object-fit: contain; } +.topbar-title { + font-size: 15px; font-weight: 600; margin: 0; - letter-spacing: -0.02em; + letter-spacing: -0.01em; white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - padding-right: 140px; } -.header-logo { - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - height: 44px; - width: auto; - max-width: 120px; - object-fit: contain; - pointer-events: none; + +.app-topbar-actions { + display: flex; + align-items: center; + gap: 6px; + margin-left: auto; + flex-wrap: wrap; +} + +.toolbar-group { display: flex; align-items: center; gap: 4px; } + +.toolbar-divider { + width: 1px; + height: 22px; + background: var(--line-soft); + flex-shrink: 0; +} + +@media (max-width: 768px) { + .app-topbar-actions { + order: 4; + flex: 1 1 100%; + margin-left: 0; + overflow-x: auto; + padding-bottom: 2px; + } } .header-toggle { @@ -103,20 +152,98 @@ header h1 { justify-content: center; } -.header-toggle-icon { font-size: 16px; } - -@media (max-width: 520px) { - .header-logo { height: 36px; max-width: 80px; } - header h1 { font-size: 16px; padding-right: 100px; } +.theme-toggle-circle { + width: 34px; + height: 34px; + border-radius: 50%; + padding: 0; + display: inline-flex; + align-items: center; + justify-content: center; + background: var(--surface-sunk); + border: 1px solid var(--line-soft); + color: var(--text); + cursor: pointer; + flex-shrink: 0; + transition: background .2s var(--ease), color .2s var(--ease), transform .1s var(--ease); } -/* === Seiten-Menü (links ausfahrend) === */ +.theme-toggle-circle:hover { background: var(--line-soft); } +.theme-toggle-circle:active { transform: scale(.95); } + +.icon-btn-danger:hover { background: var(--red); color: #fff; } +.icon-btn-danger:active { background: var(--red-active); } + +/* Konto-Menü (Avatar-Knopf rechts im Menüband + Dropdown) */ +.topbar-user { position: relative; } + +.user-menu-dropdown { + position: absolute; + top: calc(100% + 8px); + right: 0; + min-width: 220px; + background: var(--surface); + border: 1px solid var(--line-soft); + border-radius: var(--r-lg); + box-shadow: var(--sh-3); + padding: 8px; + display: flex; + flex-direction: column; + gap: 2px; + z-index: 150; + opacity: 0; + visibility: hidden; + transform: translateY(-4px); + transition: opacity .15s var(--ease), transform .15s var(--ease), visibility .15s var(--ease); +} + +.user-menu-dropdown.open { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +.user-menu-info { + padding: 8px 10px 10px; + margin-bottom: 2px; + border-bottom: 1px solid var(--line-soft); + font-size: 12px; + color: var(--text-muted); +} + +/* .menu-item: generischer Zeilen-Knopf/-Link (Text + optionales Icon) — + genutzt im Konto-Dropdown (dort über .user-menu-dropdown volle Breite) UND + als Basis für die Sidebar-Unterpunkte (siehe .sidebar-subitem). */ +a.menu-item, +button.menu-item { + display: inline-flex; + align-items: center; + gap: 8px; + text-align: left; + background: transparent; + color: var(--text); + padding: 8px 10px; + border: none; + border-radius: var(--r-md); + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: background .15s var(--ease); +} + +a.menu-item:hover, +button.menu-item:hover { background: var(--surface-sunk); } + +.user-menu-dropdown a.menu-item, +.user-menu-dropdown button.menu-item { display: flex; width: 100%; } + +/* === Seiten-Navigation (links, reine Modul-Navigation) === */ .side-menu { position: fixed; top: 0; left: 0; bottom: 0; - width: 280px; + width: 260px; max-width: 80vw; background: var(--surface); border-right: 1px solid var(--line-soft); @@ -132,70 +259,42 @@ header h1 { transform: translateX(0); } -.side-menu-header { +.side-menu-mobile-header { display: flex; - align-items: center; - justify-content: space-between; - padding: 14px 18px; - border-bottom: 1px solid var(--line-soft); - font-weight: 600; - font-size: 16px; + justify-content: flex-end; + padding: 8px 8px 0; +} +@media (min-width: 769px) { .side-menu-mobile-header { display: none; } } + +.sidebar-nav { + flex: 1; + min-height: 0; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 2px; + padding: 12px; } -.side-menu-header-actions { - display: flex; - align-items: center; - gap: 8px; -} - -.theme-toggle-circle { - width: 34px; - height: 34px; - border-radius: 50%; - padding: 0; - display: inline-flex; +.sidebar-collapse-bar { + flex-shrink: 0; + display: none; align-items: center; justify-content: center; - background: var(--surface-sunk); - border: 1px solid var(--line-soft); - color: var(--text); - cursor: pointer; - transition: background .2s var(--ease), transform .1s var(--ease); -} - -.theme-toggle-circle:hover { background: var(--line-soft); } -.theme-toggle-circle:active { transform: scale(.95); } - -.side-menu-content { - padding: 16px; - display: flex; - flex-direction: column; - gap: 18px; - overflow-y: auto; -} - -.menu-section { - display: flex; - flex-direction: column; - gap: 8px; -} - -.menu-label { - font-size: 11px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: .04em; - color: var(--text-muted); - margin-bottom: 2px; -} - -.menu-section button { width: 100%; justify-content: center; } - -.menu-divider { + width: 100%; + padding: 12px; + background: transparent; border: none; border-top: 1px solid var(--line-soft); - margin: 0; + border-radius: 0; + color: var(--text-muted); + cursor: pointer; + transition: background .2s var(--ease), color .2s var(--ease); } +.sidebar-collapse-bar:hover { background: var(--surface-sunk); color: var(--text); } +.sidebar-collapse-bar:active { transform: none; } + +#sidebarCollapseIcon { transition: transform .2s var(--ease); } /* === Show Mode === */ .show-mode-overlay { @@ -216,6 +315,8 @@ header h1 { border-bottom: 1px solid var(--line-soft); font-size: 18px; font-weight: 600; + line-height: 1.2; + letter-spacing: -0.01em; } .show-mode-content { @@ -254,33 +355,6 @@ header h1 { font-size: 12px; } -.show-mode-content .table-wrap { - max-height: none; - overflow: visible; -} - -.show-mode-content table { - font-size: 10px !important; -} - -.show-mode-content th, -.show-mode-content td { - padding: 3px 4px !important; -} - -.show-mode-content thead tr:nth-child(1) th, -.show-mode-content thead tr:nth-child(2) th { - position: static; -} - -.show-mode-content th.rot { - height: 60px !important; -} - -.show-mode-content .proj-name { - min-width: 120px !important; -} - .show-mode-content .status { min-width: 160px !important; } @@ -291,55 +365,28 @@ header h1 { font-size: 10px !important; } -.show-mode-content .person-cell { - width: 22px !important; -} - -.show-mode-content .lph-cell { - width: 38px !important; -} - -.show-mode-content .lph-cell input { - width: 30px !important; -} - .show-mode-content .bar { height: 3px !important; margin-top: 2px !important; } -.show-mode-content .group-header td { - padding: 4px 6px !important; -} - -.show-mode-content #boardLegend { - display: none; -} .show-mode-content .load-grid { - gap: 12px; + gap: 14px; } -.show-mode-content .load-row { - grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)) !important; - gap: 10px !important; +/* Im Show Mode enger: Die Auslastung wird dort auf eine Wandfläche geworfen, + auf der alle Rollen zusammen sichtbar bleiben sollen. */ +.show-mode-content .load-zeile { + padding: 7px 4px !important; } -.show-mode-content .load-card { - padding: 10px 12px !important; -} - -.show-mode-content .load-card h3 { +.show-mode-content .load-wer-name { font-size: 12px !important; } -.show-mode-content .load-num { - font-size: 22px !important; -} - -.show-mode-content .load-detail { - font-size: 10px !important; - margin-top: 6px !important; +.show-mode-content .load-wert { + font-size: 16px !important; } .show-mode-loading { @@ -358,8 +405,8 @@ header h1 { } /* Show Mode: Bearbeitung deaktivieren */ -.show-mode-content .person-cell, -.show-mode-content .lph-cell input, +.show-mode-content .board-avatar, +.show-mode-content .board-lph-row input, .show-mode-content .status-edit { pointer-events: none !important; cursor: default !important; @@ -395,6 +442,66 @@ header h1 { visibility: visible; } +/* Ab 769px: Sidebar dauerhaft sichtbar (kein Off-Canvas mehr nötig) */ +@media (min-width: 769px) { + #headerMenu.side-menu { + position: sticky; + /* top muss der tatsächlichen Höhe von .app-topbar entsprechen (beide + sticky, gleicher Scroll-Container) — sonst zieht die Sidebar bei + Seiten, die länger als ein Bildschirm sind, beim Herunterscrollen mit + top:0 unter das Menüband statt sich direkt darunter einzureihen, und + am unteren Rand bleibt eine sidebarlose Lücke in Höhe des Menübands + übrig (height: calc(100vh - 52px) reichte dann nicht bis nach unten). */ + top: 52px; + height: calc(100vh - 52px); + transform: none; + box-shadow: none; + width: 260px; + flex: 0 0 260px; + border-right: 1px solid var(--line-soft); + } + #headerMenu.side-menu .header-toggle { display: none; } + .side-menu-backdrop { display: none; } + + /* Umschalt-Leiste am unteren Sidebar-Rand nur auf Desktop-Breite sichtbar + (auf Mobile bleibt es beim reinen Off-Canvas-Menü ohne Zwischenzustand) */ + .sidebar-collapse-bar { display: flex; } + + /* Eingeklappte Sidebar: nur noch eine schmale Icon-Rail mit den 5 + Navigationspunkten, alle Beschriftungen verschwinden */ + #headerMenu.side-menu.collapsed { + width: 64px; + flex: 0 0 64px; + } + + #headerMenu.side-menu.collapsed .sidebar-nav { + padding-left: 8px; + padding-right: 8px; + } + + #headerMenu.side-menu.collapsed .nav-item .nav-label { + display: none; + } + + #headerMenu.side-menu.collapsed .nav-item { + justify-content: center; + padding-left: 0; + padding-right: 0; + } + + #headerMenu.side-menu.collapsed #sidebarCollapseIcon { + transform: rotate(180deg); + } + + /* In der Icon-Rail ergeben aufgeklappte Unterlisten/Rubrik-Überschriften + ohne Textbeschriftung keinen Sinn — beides wird vollständig ausgeblendet. */ + #headerMenu.side-menu.collapsed .nav-chevron, + #headerMenu.side-menu.collapsed .sidebar-section-label, + #headerMenu.side-menu.collapsed .sidebar-subnav { + display: none; + } +} + .toolbar { display: flex; gap: 8px; @@ -408,36 +515,17 @@ button.active { border-color: var(--accent); } -.week-date { - color: var(--text-muted); - font-size: 13px; - margin-left: 4px; -} - -/* Abteilungs-Tabs: Segmented Control */ -.dept-tabs { - position: relative; - display: flex; - align-items: stretch; - justify-content: center; - min-height: 54px; - background: var(--bg); - padding: 0; -} - +/* Hamburger-Knopf im globalen Menüband (nur < 769px sichtbar, öffnet die + Sidebar als Off-Canvas-Menü, siehe .mobile-nav-toggle weiter oben). */ .menu-corner-btn { - position: absolute; - left: 0; - top: 0; - bottom: 0; - width: 54px; display: inline-flex; align-items: center; justify-content: center; - background: var(--bg); - border: none; - border-right: 1px solid var(--line-soft); - border-radius: 0; + width: 40px; + height: 40px; + background: var(--surface-sunk); + border: 1px solid var(--line-soft); + border-radius: var(--r-md); box-shadow: none; color: var(--text); cursor: pointer; @@ -445,72 +533,122 @@ button.active { transition: background .2s var(--ease); } -.menu-corner-btn:hover { background: var(--surface-sunk); } +.menu-corner-btn:hover { background: var(--line-soft); } .menu-corner-btn:active { transform: none; } -.dept-tabs-center { +/* Hauptnavigation (Sidebar) — ersetzt die alte horizontale .tabs-Leiste */ +.sidebar-nav { gap: 2px; } + +.nav-item { display: flex; align-items: center; - gap: 2px; - flex-wrap: wrap; - padding: 12px 24px 0 24px; -} - -.dept-tab { + gap: 8px; + width: 100%; + text-align: left; + text-decoration: none; background: transparent; color: var(--text-muted); - padding: 10px 22px; + padding: 9px 12px; cursor: pointer; border: none; border-radius: var(--r-md); - font-size: 15px; + font-size: 13px; font-weight: 500; transition: background .2s var(--ease), color .2s var(--ease); } -.dept-tab:hover { - background: rgba(0,0,0,.045); - color: var(--text); +.nav-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + flex-shrink: 0; + font-size: 15px; } -.dept-tab.active { - background: var(--surface); - color: var(--text); - font-weight: 600; - box-shadow: var(--sh-1); +.nav-label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -/* Unter-Tabs */ -.tabs { - display: flex; - gap: 2px; - background: var(--bg); - padding: 6px 24px 0 24px; - flex-wrap: wrap; - border-bottom: 1px solid var(--line-soft); -} +/* .nav-item kann ein echter -Link sein statt eines Buttons — ohne + diese Regel würde ein bereits besuchtes Ziel (echte Browser-Historie) mit + der UA-Standardfarbe für besuchte Links abweichen. */ +.nav-item:visited { color: var(--text-muted); } +.nav-item:hover, +.nav-item:visited:hover { background: var(--surface-sunk); color: var(--text); } -.tab { - background: transparent; - color: var(--text-muted); - padding: 8px 14px; - cursor: pointer; - border: none; - border-radius: var(--r-sm) var(--r-sm) 0 0; - font-size: 13px; - font-weight: 500; - transition: color .2s var(--ease), box-shadow .2s var(--ease); -} - -.tab:hover { color: var(--text); } - -.tab.active { - background: transparent; +.nav-item.active { + background: var(--accent-soft); color: var(--accent); font-weight: 600; - box-shadow: inset 0 -2px 0 var(--accent); } +/* .sidebar-section-label: kleine Rubrik-Überschrift oberhalb einer Gruppe von + Sidebar-Einträgen (z. B. "Dashboard", "Navigation"). */ +.sidebar-section-label { + padding: 10px 12px 2px; + font-size: 11px; + font-weight: 700; + letter-spacing: .04em; + text-transform: uppercase; + color: var(--text-muted); +} + +/* .nav-group-toggle: Sidebar-Eintrag, der statt eines Tabs einen + auf-/zuklappbaren Unterbereich steuert (Einstellungen/Admin, siehe + toggleSidebarAccordion() in js/ui.js). */ +.nav-chevron { + display: inline-flex; + align-items: center; + justify-content: center; + width: 14px; + flex-shrink: 0; + margin-left: auto; + transition: transform .2s var(--ease); +} + +.nav-group-toggle.expanded .nav-chevron { transform: rotate(180deg); } + +/* .sidebar-subnav: Liste der Aktionen unter einem aufgeklappten + .nav-group-toggle, per max-height animiert ein-/ausgeblendet. */ +.sidebar-subnav { + display: flex; + flex-direction: column; + gap: 1px; + padding-left: 8px; + max-height: 0; + opacity: 0; + overflow: hidden; + transition: max-height .25s var(--ease), opacity .2s var(--ease); +} + +.sidebar-subnav.open { + /* Reicht für die längste Gruppe („Einstellungen", sieben Punkte); die + Animation läuft über max-height, deshalb steht hier eine feste Zahl. */ + max-height: 360px; + opacity: 1; +} + +.sidebar-subitem { + width: 100%; + padding-left: 34px; + font-size: 12.5px; + color: var(--text-muted); +} +.sidebar-subitem:hover { color: var(--text); } +/* .is-active: der gerade gezeigte Abschnitt der Gruppe „Einstellungen" + (showSettingsSection() in js/settings/settings.js). Die übrigen Gruppen + lösen Aktionen aus und kennen deshalb keinen dauerhaft aktiven Punkt. */ +.sidebar-subitem.is-active { + color: var(--accent); + font-weight: 600; + background: var(--accent-soft); +} +.sidebar-subitem.danger { color: var(--red); } +.sidebar-subitem.danger:hover { background: var(--red); color: #fff; } + /* === Buttons & Formulare === */ button { cursor: pointer; @@ -543,8 +681,8 @@ button.secondary:hover { background: rgba(0,0,0,.10); } button.secondary:active { background: rgba(0,0,0,.14); } button.danger { background: var(--red); } -button.danger:hover { background: #e8112d; } -button.danger:active { background: #b8000f; } +button.danger:hover { background: var(--red-hover); } +button.danger:active { background: var(--red-active); } button.small { padding: 3px 10px; @@ -553,6 +691,7 @@ button.small { select, input[type=text], +input[type=email], input[type=date] { padding: 7px 10px; border: 1px solid var(--line); @@ -566,6 +705,7 @@ input[type=date] { select:focus, input[type=text]:focus, +input[type=email]:focus, input[type=date]:focus { outline: none; border-color: var(--accent); @@ -627,46 +767,8 @@ thead tr:nth-child(1) th { z-index: 10; } -thead tr:nth-child(2) th { - background: var(--surface-alt); - color: var(--text); - font-weight: 500; - border: 1px solid var(--line); - position: sticky; - top: 0; /* Wird von adjustStickyOffsets() dynamisch überschrieben */ - z-index: 6; -} - -th.rot { - background: var(--surface); - writing-mode: vertical-rl; - transform: rotate(180deg); - height: 90px; - padding: 4px 2px; - font-weight: 500; -} - -td.left { - text-align: left; - min-width: 80px; -} - tr:nth-child(even) td { background: var(--surface-alt); } -.proj-name { - font-weight: 600; - min-width: 160px; - letter-spacing: -0.01em; -} - -.group-header td { - background: var(--surface-sunk) !important; - font-weight: 600; - text-align: left; - color: var(--text); - letter-spacing: -0.01em; -} - /* === Status-Felder in der Projektzeile === */ .status { min-width: 240px; @@ -704,6 +806,157 @@ tr:nth-child(even) td { background: var(--surface-alt); } .status-edit b, .status-edit strong { font-weight: 600; } +/* === Kommentar-Editor im Board und im Wochenbericht ====================== + Ersetzt das frühere contenteditable-Feld samt execCommand-Leiste: Textfeld + mit Verfasser-Kopfzeile, ausdrücklichem Speichern und dem eingeklappten, + scrollbaren Verlauf der früheren Sitzungen. Aufgebaut von + renderCommentEditor() in js/board/board.js. */ + +.comment-block { display: flex; flex-direction: column; gap: 10px; } + +.comment-editor { + border: 1px solid var(--line); + border-radius: var(--r-md); + background: var(--surface); + overflow: hidden; +} +.comment-editor-head, +.comment-head-line { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + padding: 6px 10px; + font-size: 11px; + color: var(--text-muted); + background: var(--surface-alt); + border-bottom: 1px solid var(--line-soft); +} +.comment-head-line { border: 1px solid var(--line-soft); border-radius: var(--r-md); } +.comment-who { display: flex; align-items: center; gap: 6px; color: var(--text); font-weight: 500; } +.comment-unknown { color: var(--text-subtle); font-weight: 400; font-style: italic; } +.comment-sep { color: var(--text-subtle); } +.comment-av { + width: 19px; height: 19px; border-radius: 50%; + background: var(--accent); color: #fff; + display: flex; align-items: center; justify-content: center; + font-size: 9px; font-weight: 600; flex-shrink: 0; +} +.comment-kbd { margin-left: auto; font-size: 10px; color: var(--text-subtle); white-space: nowrap; } + +.comment-editor textarea { + width: 100%; + display: block; + border: none; + border-radius: 0; + padding: 8px 10px; + font-family: inherit; + font-size: 12px; + line-height: 1.5; + color: var(--text); + background: var(--surface); + resize: vertical; + min-height: 62px; +} +.comment-editor textarea:focus { outline: none; box-shadow: none; } +.comment-editor textarea::placeholder { color: var(--text-subtle); } + +.comment-editor-foot { + display: flex; + align-items: center; + gap: 7px; + padding: 7px 10px; + border-top: 1px solid var(--line-soft); + background: var(--surface-alt); +} +.comment-foot-spacer { margin-left: auto; } +.comment-dirty { font-size: 10.5px; color: var(--amber); white-space: nowrap; } + +.comment-readonly { + border: 1px solid var(--line-soft); + border-radius: var(--r-md); + padding: 7px 9px; + font-size: 11.5px; + line-height: 1.45; + background: var(--surface); +} + +/* Verlauf: eingeklappt, mit Anzahl im Kopf; die Liste selbst scrollt */ +.comment-history { + border: 1px solid var(--line-soft); + border-radius: var(--r-md); + background: var(--surface); + overflow: hidden; +} +.comment-history > summary { + cursor: pointer; + list-style: none; + display: flex; + align-items: center; + gap: 8px; + padding: 7px 10px; + font-size: 11.5px; + color: var(--text); + user-select: none; +} +.comment-history > summary::-webkit-details-marker { display: none; } +.comment-history > summary:hover { background: var(--surface-alt); } +.comment-history-caret { + color: var(--text-subtle); + font-size: 9px; + transition: transform .18s var(--ease); +} +.comment-history[open] > summary .comment-history-caret { transform: rotate(90deg); } +.comment-history-count { + margin-left: auto; + font-size: 10.5px; + font-weight: 600; + background: var(--accent-soft); + color: var(--accent); + border-radius: var(--r-pill); + padding: 1px 8px; + font-variant-numeric: tabular-nums; +} +.comment-history-count.zero { background: var(--surface-sunk); color: var(--text-subtle); } +.comment-history-body { border-top: 1px solid var(--line-soft); padding: 4px 10px 8px; } + +.comment-history-list { + max-height: 220px; + overflow-y: auto; + overscroll-behavior: contain; +} +.comment-history-entry { padding: 8px 0; } +.comment-history-entry + .comment-history-entry { border-top: 1px solid var(--line-soft); } +.comment-history-head { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 3px; + font-size: 10.5px; +} +.comment-history-week { + font-weight: 600; + color: var(--text); + white-space: nowrap; + font-variant-numeric: tabular-nums; +} +.comment-history-meta { color: var(--text-subtle); } +.comment-history-text { font-size: 11.5px; line-height: 1.45; color: var(--text-muted); } +.comment-history-text b, +.comment-history-text strong { font-weight: 600; color: var(--text); } + +.comment-take { + margin-left: auto; + padding: 2px 9px; + font-size: 10px; + border-radius: var(--r-pill); + background: transparent; + color: var(--text-muted); + border: 1px solid var(--line); + flex-shrink: 0; +} +.comment-take:hover { background: var(--surface-sunk); color: var(--text); } + .status-toolbar { display: none; position: absolute; @@ -745,26 +998,6 @@ tr:nth-child(even) td { background: var(--surface-alt); } font-weight: 600; } -/* === Personen-Zuweisungszellen === */ -.person-cell { - cursor: pointer; - user-select: none; - width: 26px; - transition: background .12s var(--ease); -} - -.person-cell.on { - background: var(--green) !important; - color: #fff; - font-weight: 600; -} - -.person-cell.optional { - background: #d18b00 !important; - color: #fff; - font-weight: 600; -} - .locked-cell { cursor: not-allowed !important; background: var(--surface-sunk) !important; @@ -777,70 +1010,6 @@ tr:nth-child(even) td { background: var(--surface-alt); } border-color: var(--line-soft) !important; } -/* Gesperrte Zellen immer grau, auch wenn X/(X) gesetzt ist */ -.person-cell.locked-cell.on, -.person-cell.locked-cell.optional { - background: var(--surface-sunk) !important; - color: var(--text-muted) !important; -} - -/* === Rollenfarben === */ -.person-cell.role-planung { - background: linear-gradient(rgba(255,255,255,.55), rgba(255,255,255,.55)), - rgb(212, 245, 224); - background-attachment: fixed; -} - -.person-cell.role-projektleitung { - background: linear-gradient(rgba(255,255,255,.55), rgba(255,255,255,.55)), - rgb(255, 219, 219); - background-attachment: fixed; -} - -.person-cell.role-bauüberwachung { - background: linear-gradient(rgba(255,255,255,.55), rgba(255,255,255,.55)), - rgb(203, 238, 255); - background-attachment: fixed; -} - -.role-planung, -.role-planung.rot { - background: linear-gradient(rgba(255,255,255,.55), rgba(255,255,255,.55)), - rgb(212, 245, 224); - background-attachment: fixed; -} - -.role-projektleitung, -.role-projektleitung.rot { - background: linear-gradient(rgba(255,255,255,.55), rgba(255,255,255,.55)), - rgb(255, 219, 219); - background-attachment: fixed; -} - -.role-bauüberwachung, -.role-bauüberwachung.rot { - background: linear-gradient(rgba(255,255,255,.55), rgba(255,255,255,.55)), - rgb(203, 238, 255); - background-attachment: fixed; -} - -th.role-planung, -th.role-projektleitung, -th.role-bauüberwachung { color: var(--text); } - -/* === LPH / Balken / Überschriften === */ -.lph-cell { width: 50px; } - -.lph-cell input { - width: 38px; - border: 1px solid var(--line-soft); - border-radius: var(--r-sm); - padding: 3px; - text-align: center; - font-size: 11px; - font-family: inherit; -} - .bar { height: 5px; border-radius: var(--r-pill); @@ -856,16 +1025,449 @@ th.role-bauüberwachung { color: var(--text); } transition: width .3s var(--ease); } -/* Markierung der amtierenden Leistungsphase (gesetzt im Tab "Projekte") */ -.lph-cell.lp-aktiv { - background: var(--accent-soft); - box-shadow: inset 0 2px 0 var(--accent); +/* === Projektboard (Listenansicht) === + Eine Bubble-Card je Projektgruppe (.board-list) mit den Projekten als + Listenzeilen (.board-row) — ersetzt die frühere breite Matrix (eine Spalte + je Person/Leistungsphase). Jede Zeile zeigt nur das Nötigste auf einen + Blick (Kürzel-Avatare der Zugeordneten, aktive Phase, Status-Vorschau); + Klick auf die Zeile klappt darunter das Detailpanel (.board-drawer) mit + Status-Editor, vollständiger Rollen-Zuordnung und allen LPH-Balken auf. */ +.board-list { + display: flex; + flex-direction: column; + background: var(--surface); + border: 1px solid var(--line-soft); + border-radius: var(--r-xl); + box-shadow: var(--sh-2); + overflow: hidden; } -.lph-cell.lp-aktiv input { - border-color: var(--accent); - font-weight: 600; +.board-row { border-bottom: 1px solid var(--line-soft); } +.board-row:last-child { border-bottom: none; } + +.board-row-main { + display: flex; + align-items: center; + gap: 14px; + padding: 12px 16px; + cursor: pointer; + transition: background .15s var(--ease); } +.board-row-main:hover { background: var(--surface-sunk); } + +.board-chev { + flex-shrink: 0; + color: var(--text-subtle); + display: inline-flex; + transition: transform .2s var(--ease); +} +.board-row.open .board-chev { transform: rotate(90deg); } + +.board-nr { + flex-shrink: 0; + width: 64px; + font-size: 11.5px; + color: var(--text-muted); + font-family: ui-monospace, 'SF Mono', monospace; +} + +.board-name-wrap { flex: 1 1 260px; min-width: 0; } +.board-name { + font-weight: 600; + letter-spacing: -0.01em; + cursor: pointer; +} +.board-name:hover { color: var(--accent); text-decoration: underline; } + +.board-status-line { + margin-top: 3px; + font-size: 12px; + color: var(--text-muted); + display: flex; + align-items: center; + gap: 6px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.board-newdot { + flex-shrink: 0; + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--green); +} + +.board-avatar-stack { + flex-shrink: 0; + display: flex; + flex-wrap: wrap; + gap: 4px; + max-width: 220px; +} + +.board-avatar { + flex-shrink: 0; + width: 26px; + height: 26px; + border-radius: 50%; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 10px; + font-weight: 700; + letter-spacing: -0.02em; + color: var(--text-muted); + background: var(--surface-sunk); + border: 1px solid var(--line-soft); + cursor: pointer; + user-select: none; + transition: background .15s var(--ease), color .15s var(--ease), box-shadow .15s var(--ease); +} + +.board-avatar.role-planung.state-1 { background: var(--green); border-color: var(--green); color: #fff; } +.board-avatar.role-projektleitung.state-1 { background: var(--red); border-color: var(--red); color: #fff; } +.board-avatar.role-bauüberwachung.state-1 { background: var(--accent); border-color: var(--accent); color: #fff; } + +.board-avatar.role-planung.state-2 { background: transparent; color: var(--green); border-color: var(--green); } +.board-avatar.role-projektleitung.state-2 { background: transparent; color: var(--red); border-color: var(--red); } +.board-avatar.role-bauüberwachung.state-2 { background: transparent; color: var(--accent); border-color: var(--accent); } + +.phase-pill { + flex-shrink: 0; + font-size: 11px; + font-weight: 600; + font-family: ui-monospace, 'SF Mono', monospace; + padding: 4px 10px; + border-radius: var(--r-pill); + background: var(--accent-soft); + color: var(--accent); + white-space: nowrap; +} +.phase-pill.muted { background: var(--surface-sunk); color: var(--text-subtle); font-family: inherit; font-weight: 500; } + +/* --- Detailpanel (.board-drawer) --- */ +.board-drawer { + padding: 18px 20px 22px 94px; + background: var(--surface-alt); + border-top: 1px solid var(--line-soft); +} + +.board-drawer h4 { + margin: 0 0 8px; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .05em; + color: var(--text-muted); +} + +/* Die Schublade liefert drei gleichrangige Blöcke (Kommentar, Leistungsphasen, + Beteiligte, siehe renderBoardDrawer() in js/board/board.js). Im Projektboard + stehen Kommentar und Leistungsphasen wie bisher übereinander in der linken + Spalte, die Beteiligten rechts daneben. */ +.board-drawer-grid { + display: grid; + grid-template-columns: 1.3fr 1fr; + grid-template-areas: + "kommentar beteiligte" + "lph beteiligte"; + align-content: start; + gap: 16px 24px; +} +.drawer-col-comment { grid-area: kommentar; } +.drawer-col-lph { grid-area: lph; } +.drawer-col-people { grid-area: beteiligte; } + +@media (max-width: 720px) { + .board-drawer-grid { + grid-template-columns: 1fr; + grid-template-areas: "kommentar" "lph" "beteiligte"; + } +} + +/* Auswahl der beauftragten Leistungsphasen. Genutzt vom Honorar-Dialog der + Projektplan-Seite (renderHonorarEingaben(), js/plan/plan-honorar.js) und + vom Neues-Projekt-Modal (openNewProjModal(), js/load/load.js) — deshalb + hier im gemeinsamen Stylesheet statt inline in einer der beiden Seiten. */ +.beauftragte-lp-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(84px, 1fr)); + gap: 4px 10px; +} +.beauftragte-lp-item { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: var(--text); + white-space: nowrap; +} + +/* --- Wochenbericht: immer aufgeklappte Projekt-Karten ("Neu kommentiert", + "Fortschritt aktualisiert") — im Unterschied zu .board-list/.board-row + (Akkordeon der "Unverändert"-Gruppe) ist hier jede Karte ein eigenständiger + Kasten, kein Container mit Trennlinien. */ +.report-list { + display: flex; + flex-direction: column; + gap: 10px; +} +.report-card { + background: var(--surface); + border: 1px solid var(--line-soft); + border-radius: var(--r-md); + box-shadow: var(--sh-1); + overflow: hidden; +} + +/* Kopfband: durchgefärbt in der Farbe der Änderungsart, damit beim Scrollen + ohne Blick auf die Abschnitts-Überschrift erkennbar ist, worum es geht. + Die Farbe kommt aus --band-neu/--band-fortschritt und lässt sich dort in + einem Zug abschwächen, falls die Leiste zu kräftig wirkt. */ +.report-card-head { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + padding: 9px 14px; + background: var(--surface-alt); + border-bottom: 1px solid var(--line-soft); +} +.report-card-head .board-name { cursor: pointer; } + +.report-card-neu > .report-band { background: var(--band-neu); } +.report-card-fortschritt > .report-band { background: var(--band-fortschritt); } + +.report-band { + border-bottom: none; + color: #fff; +} +.report-band .board-nr { color: rgba(255,255,255,.75); } +.report-band .board-name { color: #fff; font-weight: 600; } +.report-band .board-name:hover { text-decoration: underline; } +.report-band .tag-chip { + background: rgba(255,255,255,.22); + color: #fff; + border-color: transparent; +} +.report-band .report-reason-badge { + background: rgba(255,255,255,.22); + color: #fff; +} + +/* Drei Spalten statt zwei: im Bericht stehen Leistungsphasen und Beteiligte + nebeneinander, wodurch die Karte flacher wird. Dasselbe Markup wie im + Projektboard, nur anders angeordnet. */ +.report-card .board-drawer { + padding: 14px 16px 16px; + border-top: none; + background: var(--surface); +} +.report-card .board-drawer-grid { + grid-template-columns: 1.7fr 1fr 1fr; + grid-template-areas: "kommentar lph beteiligte"; + gap: 22px; +} +.report-card .board-role-label { width: 58px; } + +@media (max-width: 860px) { + .report-card .board-drawer-grid { + grid-template-columns: 1fr; + grid-template-areas: "kommentar" "lph" "beteiligte"; + gap: 16px; + } +} + +/* === Dichte Fassung der Karte (.board-drawer-dicht, Wochenbericht) === + Gleiche Angaben, gleiche Bedienelemente, enger gesetzt. Den Ausschlag gibt + die Leistungsphasen-Leiste: als Zeilen untereinander bestimmten bis zu zehn + Phasen die Höhe der ganzen Karte — bei 29 Projekten wurde der Bericht damit + rund 17 Bildschirmseiten lang. Nebeneinander braucht dieselbe Information + eine Zeile. */ +.board-drawer-dicht .board-drawer-grid { + /* Die Beteiligten brauchen so viel Breite wie der Kommentar: Sobald ihre + Kürzel umbrechen, bestimmt ihre Spalte die Höhe der ganzen Karte. Mit + 1.45/1.15/1 waren es 14,2 Bildschirmseiten, mit gleich breiter + Beteiligten-Spalte 11,6 — mehr Breite bringt darüber hinaus nichts, + weil dann ohnehin nichts mehr umbricht. */ + /* Die rechte Spalte bekommt eine MINDESTBREITE: Zehn Phasenfelder in einer + reinen fr-Spalte wurden 27 px schmal, und "100" passte nicht mehr ins + Eingabefeld — 79 Werte waren abgeschnitten. Unter 620 px Containerbreite + gibt die Mindestbreite nach und die Spalten rutschen untereinander. */ + grid-template-columns: minmax(0, 1fr) minmax(340px, 1fr); + grid-template-areas: "kommentar kompakt"; + gap: 18px; +} +@media (max-width: 860px) { + .board-drawer-dicht .board-drawer-grid { + grid-template-columns: 1fr; + grid-template-areas: "kommentar" "kompakt"; + gap: 14px; + } +} +.drawer-col-kompakt { grid-area: kompakt; min-width: 0; } +/* Die zweite Überschrift in derselben Spalte braucht Abstand nach oben, den + die erste (am Spaltenanfang) nicht haben darf. */ +.drawer-h4-zweit { margin-top: 12px; } + +.board-lph-leiste { + display: flex; + gap: 3px; + align-items: stretch; +} +.board-lph-cell { + flex: 1 1 0; + min-width: 0; + display: flex; + flex-direction: column; + align-items: center; + gap: 1px; + padding: 3px 2px 4px; + border: 1px solid var(--line-soft); + border-radius: var(--r-sm); + background: var(--surface-sunk); + /* Der Füllstand kommt als senkrechter Verlauf aus dem Inline-Stil + (renderBoardDrawer in js/board/board.js) — so bleibt der Balken erhalten, + den die Zeilenform als eigenes Element hatte. */ + background-repeat: no-repeat; +} +.board-lph-cell-label { + font-size: 10px; + font-weight: 600; + color: var(--text-muted); + line-height: 1.2; +} +.board-lph-cell input { + width: 100%; + min-width: 0; + padding: 1px 2px; + border: none; + background: transparent; + text-align: center; + font-size: 11.5px; + font-variant-numeric: tabular-nums; + color: var(--text); + font-family: inherit; +} +.board-lph-cell input:focus { + outline: 1.5px solid var(--accent); + outline-offset: -1px; + border-radius: 3px; +} +.board-lph-cell.lp-aktiv { + border-color: var(--accent); +} +.board-lph-cell.lp-aktiv .board-lph-cell-label, +.board-lph-cell.lp-aktiv input { color: var(--accent); } +.board-lph-cell.locked-cell input { cursor: default; } + +/* Das Rollen-Label steht NEBEN seinen Kürzeln. Über ihnen kostete es drei + zusätzliche Zeilen, und damit bestimmte die Beteiligten-Spalte die Höhe der + ganzen Karte (189 px gegenüber 130 für den Kommentar). Die feste Breite ist + so bemessen, dass "Bauüberwachung" hineinpasst — bei 52 px lief die + Beschriftung über die Kürzel. + + Die Regeln hängen am Merkmal .board-drawer-dicht, nicht am Ort: Sie + standen zunächst unter .report-card, und mit dem Umbau des Wochenberichts + auf die Tagesordnung gab es diesen Container nicht mehr — die dichte + Karte fiel still auf die Board-Darstellung zurück. Das zusätzliche + .board-role-mini liefert die Spezifität, die nötig ist, um die dortige + Regel für .board-role-row zu schlagen. */ +.board-drawer-dicht .board-role-mini .board-role-row { + display: flex; + align-items: flex-start; + gap: 7px; + margin-bottom: 4px; +} +.board-drawer-dicht .board-role-mini .board-role-label { + flex: 0 0 auto; + /* Bemessen am längsten Rollennamen: "BAUÜBERWACHUNG" braucht bei 10 px in + Versalien diese Breite — bei 88 px fehlte der letzte Buchstabe. */ + width: 104px; + padding-top: 4px; + font-size: 10px; + line-height: 1.25; + letter-spacing: .02em; + text-transform: uppercase; +} + +/* Schmale Fenster: Dort reicht die Breite für Label UND Kürzel nicht mehr, + also wieder übereinander — lieber drei Zeilen mehr als abgeschnittene + Beschriftungen. */ +@media (max-width: 1100px) { + .board-drawer-dicht .board-role-mini .board-role-row { display: block; } + .board-drawer-dicht .board-role-mini .board-role-label { + width: auto; + padding-top: 0; + margin-bottom: 2px; + } +} + +.report-reason-badge { + margin-left: auto; + font-size: 11px; + font-weight: 600; + padding: 3px 10px; + border-radius: var(--r-pill); + background: var(--accent-soft); + color: var(--accent); + white-space: nowrap; +} +/* Innerhalb einer Report-Karte braucht das Detailpanel nicht den + Zeilen-Einzug/die obere Trennlinie, die es im Board-Akkordeon ausrichtet. */ +.report-card .board-drawer { + padding: 16px 20px 20px; + border-top: none; +} + +.board-lph-list { + display: flex; + flex-direction: column; + gap: 7px; + margin-top: 14px; +} + +.board-lph-row { + display: grid; + grid-template-columns: 42px 1fr 50px; + align-items: center; + gap: 10px; +} + +.board-lph-row .bar { flex: 1; margin-top: 0; } + +.board-lph-label { + font-size: 11px; + font-family: ui-monospace, 'SF Mono', monospace; + color: var(--text-muted); +} + +.board-lph-row input { + width: 100%; + border: 1px solid var(--line-soft); + border-radius: var(--r-sm); + padding: 3px 6px; + text-align: center; + font-size: 11px; + font-family: inherit; + background: var(--surface); + color: var(--text); +} + +.board-lph-row.lp-aktiv .board-lph-label { color: var(--accent); font-weight: 600; } +.board-lph-row.lp-aktiv input { border-color: var(--accent); font-weight: 600; } + +.board-role-mini { display: flex; flex-direction: column; gap: 10px; } +.board-role-mini .board-role-row { display: flex; align-items: center; gap: 10px; } +.board-role-mini .board-role-label { + width: 100px; + flex-shrink: 0; + font-size: 11.5px; + color: var(--text-muted); +} +.board-role-mini .board-role-chips { display: flex; flex-wrap: wrap; gap: 6px; } .section-title { font-size: 17px; @@ -873,51 +1475,266 @@ th.role-bauüberwachung { color: var(--text); } margin: 24px 0 10px; color: var(--text); letter-spacing: -0.02em; + line-height: 1.2; } /* === Lade-/Auswertungsübersicht === */ .load-grid { display: flex; flex-direction: column; - gap: 20px; + gap: 24px; width: 100%; } .load-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); - gap: 14px; + gap: 16px; align-items: start; width: 100%; } +/* === Auslastung: das Lastband === + Eine Zeile je Person und Rolle; der Balken ist nach Projekten aufgeteilt. + Zuvor stand jede Person in einer eigenen .load-card mit ihrer Projektliste + klein darunter — man sah die Zahl, aber nicht, woraus sie besteht. Die + Frage nach einer Auslastung von 1,80 ist aber immer dieselbe: aus einem + großen Projekt oder aus fünf kleinen, und was davon lässt sich abgeben? + .load-card/.load-row/.load-num/.load-detail bleiben bestehen — das + Dashboard (js/dashboard/dashboard.js) und die Ressourcenansicht des + Projektbereichs (js/plan/plan-gantt.js) bauen darauf auf. */ +.load-rolle { width: 100%; } + +.load-rolle-titel { + font-size: 14px; + font-weight: 600; + margin: 0 0 6px; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: .04em; +} + +.load-zeile { + display: flex; + align-items: center; + gap: 16px; + padding: 11px 4px; + border-bottom: 1px solid var(--line-soft); +} +.load-zeile:last-child { border-bottom: none; } +.load-zeile:hover { background: var(--surface); } + +/* Wer — Name oben, Kürzel und Zusätze darunter. Feste Breite, damit die + Bänder aller Zeilen an derselben Stelle beginnen und vergleichbar sind. */ +.load-wer { + flex: 0 0 200px; + min-width: 0; +} +.load-wer-name { + font-size: 13px; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.load-wer-meta { + display: flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; + font-size: 11px; + color: var(--text-subtle); + margin-top: 1px; +} + +/* Das Band. Die Segmente füllen es von links; was frei bleibt, ist die + Restkapazität bis zum Skalenende (Vorgabe 8, firmenweit einstellbar). */ +.load-band { + flex: 1; + display: flex; + align-items: center; + height: 22px; + min-width: 140px; + background: var(--surface-sunk); + border-radius: var(--r-sm); + overflow: hidden; +} + +.load-seg { + display: flex; + align-items: center; + height: 100%; + padding: 0 7px; + min-width: 3px; + font-size: 10px; + font-weight: 600; + color: #fff; + white-space: nowrap; + overflow: hidden; + border-right: 1px solid rgba(255,255,255,.4); + transition: filter .15s var(--ease); +} +.load-seg:last-of-type { border-right: none; } +.load-seg:hover { filter: brightness(1.12); } +.load-seg span { overflow: hidden; } + +.load-band-leer { + font-size: 11px; + color: var(--text-subtle); + padding-left: 9px; +} + +.load-wert { + flex: 0 0 62px; + text-align: right; + font-size: 19px; + font-weight: 600; + letter-spacing: -.02em; + font-variant-numeric: tabular-nums; +} + +.load-anbahnung { + font-size: 10px; + padding: 1px 7px; + border-radius: var(--r-pill); + background: var(--surface-sunk); + color: var(--text-muted); + white-space: nowrap; +} + +/* Kopfzeile: was die Zahlen abdecken. Sie steht oben, weil die Aussagekraft + der ganzen Seite daran hängt — eine Auslastung aus drei von 29 Projekten + ist etwas anderes als eine aus 29. */ +.load-kopf { + display: flex; + align-items: center; + gap: 16px; + flex-wrap: wrap; + padding: 12px 14px; + background: var(--surface-sunk); + border-radius: var(--r-lg); + font-size: 13px; +} +.load-kopf-zahl strong { + font-size: 17px; + font-variant-numeric: tabular-nums; +} +.load-kopf-warnung { + color: var(--red); + font-weight: 600; +} +.load-kopf-warnung::before { + content: ''; + display: inline-block; + width: 7px; height: 7px; + border-radius: 50%; + background: var(--red); + margin-right: 7px; + vertical-align: middle; +} + +.load-luecken { + padding: 12px 14px; + border: 1px dashed var(--line); + border-radius: var(--r-lg); + font-size: 12px; +} +.load-luecke { + display: flex; + justify-content: space-between; + gap: 16px; + padding: 4px 0; + border-bottom: 1px solid var(--line-soft); +} +.load-luecke:last-child { border-bottom: none; } +.load-luecke b { color: var(--text-muted); font-weight: 500; white-space: nowrap; } + +/* Summenzeile je Mensch: ein durchgehender Riegel statt der Projektsegmente. + Sie beantwortet die Frage, die die Rollenzeilen nicht beantworten können — + wer in drei Rollen arbeitet, hat trotzdem nur eine Arbeitswoche. */ +.load-zeile-summe .load-wer-name { font-weight: 600; } +.load-seg-summe { border-right: none; } + +/* Marke der vollen Kapazität im Band. Sie sitzt am rechten Rand, weil das + volle Band genau die Wochenkapazität ist; ein Riegel, der sie erreicht, + stößt sichtbar an. */ +.load-kapazitaet { + position: absolute; + right: 0; top: 0; bottom: 0; + width: 2px; + background: var(--text-subtle); + opacity: .5; +} +.load-zeile-summe .load-band { position: relative; } + +.load-einheit { + font-size: 11px; + font-weight: 500; + color: var(--text-subtle); + margin-left: 2px; +} + +.load-legende-item em { + font-style: normal; + color: var(--text-subtle); + margin-left: 3px; +} + +/* Legende der Projektfarben, nur für die tatsächlich vorkommenden Projekte. + Nötig, weil in schmalen Segmenten die Projektnummer keinen Platz findet. */ +.load-legende { + display: flex; + flex-wrap: wrap; + gap: 6px 16px; + padding-top: 14px; + border-top: 1px solid var(--line-soft); + font-size: 11px; + color: var(--text-muted); +} +.load-legende-item { + display: inline-flex; + align-items: center; + gap: 6px; +} +.load-legende-item i { + width: 9px; + height: 9px; + border-radius: 2px; + flex: 0 0 auto; +} + +@media (max-width: 720px) { + .load-wer { flex-basis: 128px; } + .load-zeile { gap: 10px; } + .load-wert { flex-basis: 52px; font-size: 16px; } +} + .load-row-title { grid-column: 1 / -1; width: 100%; - font-size: 20px; + font-size: 14px; font-weight: 600; margin: 0 0 2px 0; padding-top: 4px; color: var(--text-muted); text-transform: uppercase; letter-spacing: .04em; - font-size: 14px; } .load-card { background: var(--surface); border: 1px solid var(--line-soft); - border-radius: var(--r-lg); - padding: 16px 18px; + border-radius: var(--r-xl); + padding: 20px 22px; min-width: 0; width: 100%; - box-shadow: var(--sh-1); + box-shadow: var(--sh-2); transition: box-shadow .2s var(--ease), transform .2s var(--ease); } .load-card:hover { - box-shadow: var(--sh-2); - transform: translateY(-1px); + box-shadow: var(--sh-3); + transform: translateY(-2px); } .load-card h3 { @@ -925,6 +1742,7 @@ th.role-bauüberwachung { color: var(--text); } font-size: 14px; font-weight: 600; letter-spacing: -0.01em; + line-height: 1.25; } .load-num { @@ -960,7 +1778,12 @@ th.role-bauüberwachung { color: var(--text); } display: none; align-items: center; justify-content: center; - z-index: 100; + /* Über Menüband (.app-topbar, 140) UND Seitenleiste (.side-menu, 200): + Mit dem früheren Wert 100 blieb die Navigation neben einem offenen Dialog + sichtbar und anklickbar — auch hinter dem "blockierenden" Login-Fenster — + und die Seitenleiste verdeckte bei mittleren Fensterbreiten die linke + Kante breiter Dialoge. */ + z-index: 300; } .modal { @@ -979,6 +1802,7 @@ th.role-bauüberwachung { color: var(--text); } font-size: 20px; font-weight: 600; letter-spacing: -0.02em; + line-height: 1.15; } .form-row { @@ -994,6 +1818,59 @@ th.role-bauüberwachung { color: var(--text); } color: var(--text-muted); } +/* Gesperrte Bedienelemente: Ohne eigene :disabled-Regeln schlagen die + Farbangaben oben die Browser-Voreinstellung, und ein deaktivierter Knopf + sieht exakt aus wie ein bedienbarer (z. B. in gesperrten Sitzungen oder + bei der eigenen Zeile in der Benutzerverwaltung). */ +button:disabled, +input:disabled, +select:disabled, +textarea:disabled { + opacity: .45; + cursor: not-allowed; + box-shadow: none; +} +button:disabled:hover, +select:disabled:hover { + filter: none; + transform: none; +} + +/* Warnband unten rechts, wenn das Speichern fehlschlägt (showSyncProblem() in + js/store.js). Liegt über allem, damit es auch neben einem offenen Dialog + sichtbar bleibt. */ +.sync-problem-bar { + position: fixed; + right: 18px; + bottom: 18px; + z-index: 500; + max-width: 340px; + background: var(--red); + color: #fff; + font-size: 13px; + line-height: 1.45; + padding: 11px 15px; + border-radius: var(--r-md); + box-shadow: var(--sh-3); +} + +/* Hinweisband gesperrter Sitzungen (renderBoard()/renderWochenbericht()). + Vorher standen die Farben als Inline-Angaben im Markup und waren damit im + Dunkelmodus grell weiß-rosa. */ +.lock-notice { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + background: var(--surface-alt); + border: 1px solid var(--red); + color: var(--red); + border-radius: var(--r-md); + padding: 9px 13px; + margin-bottom: 12px; + font-size: 13px; +} + /* === LP-Aufwand-Maske als Matrix-Tabelle === Zeilen = Leistungsphasen, Spalten = Rollen + "amtierend" (Radio). */ .lp-matrix { @@ -1043,25 +1920,38 @@ th.role-bauüberwachung { color: var(--text); } max-width: 640px; } +/* .bubble-card: gemeinsame "Bubble-Card"-Optik für Projekt- und + Personen-Karten (Projekte-/Projektbeteiligte-Tab) — gleicher Radius/ + Schatten wie .load-card (Dashboard/Auslastung), aber ohne Hover-Anhebung, + weil die Projekt-Karten Formularelemente enthalten (eine Anhebung beim + Überfahren wäre dort irritierend). */ +.bubble-card { + background: var(--surface); + border: 1px solid var(--line-soft); + border-radius: var(--r-xl); + padding: 20px 22px; + box-shadow: var(--sh-2); + transition: box-shadow .2s var(--ease); +} + +.bubble-card:hover { + box-shadow: var(--sh-3); +} + .person-list-item, .person-card { display: flex; flex-direction: column; - border-bottom: 1px solid var(--line-soft); width: 100%; } .person-list-item { align-items: center; gap: 8px; - padding: 8px 4px; overflow-x: auto; flex-direction: row; } -.person-card:last-child, -.person-list-item:last-child { border-bottom: none; } - .person-header { display: flex; align-items: center; @@ -1069,10 +1959,43 @@ th.role-bauüberwachung { color: var(--text); } padding: 8px 4px; cursor: pointer; overflow-x: auto; + transition: background .2s var(--ease); } .person-header:hover { background: var(--surface-sunk); } +/* Schaltflächen-Optik auch für Links: Ein in einer + Knopfleiste sah sonst aus wie ein blauer Standardlink, weil .secondary + ausschließlich für