Geviertstriche aus Text, Kommentaren und Doku entfernen
467 Stellen. Nicht durch einen anderen Strich ersetzt, sondern durch das
Satzzeichen, das an der jeweiligen Stelle richtig ist: Doppelpunkt, wo eine
Erlaeuterung folgt; Komma, wo der Satz weiterlaeuft; Punkt, wo zwei
Hauptsaetze aneinanderstiessen. Ein Halbgeviertstrich waere nur derselbe
Griff mit kuerzerem Strich gewesen.
Regelbasiert ersetzt und danach nachgeprueft, weil eine Regel allein nicht
reicht. Gefunden und einzeln korrigiert:
- Eingeschobene Saetze mit Strichpaar. Nur der oeffnende Strich passte ins
Muster, der schliessende blieb als "—," stehen. Drei Stellen, jetzt mit
Klammern oder als eigener Satz.
- Doppelte Doppelpunkte, wo beide Striche eines Paars zu Doppelpunkten wurden
("Globale Regeln: Schrift, Groessen, Abstaende: kommen aus HiveGrid").
Sieben Stellen in Doku und Oberflaeche, jetzt in Klammern.
- Pfade, denen ein Komma statt eines Doppelpunkts folgte.
- Ausgerichtete Kommentartabellen, in denen der Strich eine Spalte war.
Nicht angefasst: drei Geviertstriche, die keine Prosa sind, sondern
Tabellenplatzhalter fuer "kein Wert" (Trichter, Auftraege, Kunden). Dort ist
der Strich die Angabe selbst; ihn zu entfernen liesse leere Zellen zurueck.
tsc, lint, build, verify:carbon und verify:layout laufen sauber.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPeZNwSeU7qG3RRCwaxEZX
This commit is contained in:
1 parent
e998231fed
commit
6a0b36ddcc
75 files changed
+471
-471
No files matched your search
+12
-12
@@ -1,7 +1,7 @@
|
||||
# Übergabe an die Entwicklung
|
||||
|
||||
> **Rangfolge:** Wo dieser Prototyp von HiveGrid Cloud abweicht, **gilt
|
||||
> HiveGrid** — Schrift, Schriftgrößen, Abstände, Farben, Control-Höhen,
|
||||
> HiveGrid**: Schrift, Schriftgrößen, Abstände, Farben, Control-Höhen,
|
||||
> App-Shell. Dass hier dieselbe Schrift und dieselbe Seitenleisten-Komponente
|
||||
> stecken, dient nur der Lesbarkeit neben der Software; es ist keine Vorlage,
|
||||
> an der sich die Software auszurichten hätte. Verbindlich sind aus diesem Repo
|
||||
@@ -12,7 +12,7 @@
|
||||
> **Ebenfalls vor dem Einbau lesen:**
|
||||
> [`docs/SCOPE-ANALYTICS-DASHBOARD.md`](docs/SCOPE-ANALYTICS-DASHBOARD.md),
|
||||
> insbesondere Abschnitt 3a. Der Handlungsbedarf steht dort **absichtlich
|
||||
> doppelt** — auf dem Dashboard und auf der Analytics-Übersicht —, weil das
|
||||
> doppelt**: auf dem Dashboard und auf der Analytics-Übersicht, weil das
|
||||
> Entfernen eine offene Produktfrage mitentschieden hätte. Das ist vor dem
|
||||
> Port aufzulösen und nicht zu übernehmen.
|
||||
|
||||
@@ -25,13 +25,13 @@ Was echt ist, was Attrappe ist und was beim Einbau zu tun bleibt:
|
||||
|
||||
| Bereich | Datei(en) | Zustand |
|
||||
|---|---|---|
|
||||
| Kennzahlen-Berechnung | `features/dashboard/data/calculations.ts` | **Reine Funktionen** über typisierten Eingangsdaten — direkt übernehmbar |
|
||||
| Kennzahlen-Berechnung | `features/dashboard/data/calculations.ts` | **Reine Funktionen** über typisierten Eingangsdaten: direkt übernehmbar |
|
||||
| Chart-Theme | `features/dashboard/utils/chartTheme.ts` | Carbon-Tokens, Palette, Achsen-Props, Margins |
|
||||
| Formatierung | `features/dashboard/utils/formatters.ts` | Deutsche Zahlen-, Währungs- und Datumsformate |
|
||||
| Chart-Komponenten | `features/dashboard/components/charts/` | Vollständig, mit Leerzuständen und Tooltips |
|
||||
| Karten & Tabellen | `features/dashboard/components/cards/`, `tables/` | Vollständig |
|
||||
| Filtersystem | `features/dashboard/context/FilterContext.tsx`, `data/filtering.ts` | State und Relevanzlogik fertig |
|
||||
| Seitenaufbau | `features/dashboard/pages/` | **Reine Komponenten** `({ vm })` — kein Store-Zugriff, direkt übernehmbar |
|
||||
| Seitenaufbau | `features/dashboard/pages/` | **Reine Komponenten** `({ vm })`, kein Store-Zugriff, direkt übernehmbar |
|
||||
| View-Model-Builder | `features/dashboard/view-models/*.vm.ts` | Reine Funktionen `(daten, filter) → VMState<…>`; im Loader wiederverwendbar |
|
||||
| Seitenzustände | `components/layout/SeitenZustand.tsx` | idle · loading · empty · error ausgestaltet |
|
||||
|
||||
@@ -42,7 +42,7 @@ Was echt ist, was Attrappe ist und was beim Einbau zu tun bleibt:
|
||||
### 2.1 Datenquelle austauschen
|
||||
|
||||
Alles hängt an **einer** Stelle: `features/dashboard/data/seed.ts` exportiert
|
||||
`DATENSATZ` vom Typ `Datensatz`. Diesen Export durch echte Daten ersetzen —
|
||||
`DATENSATZ` vom Typ `Datensatz`. Diesen Export durch echte Daten ersetzen,
|
||||
Loader, Server-Query oder Service, je nach Architektur. Alle Berechnungen und
|
||||
Komponenten arbeiten unverändert weiter.
|
||||
|
||||
@@ -50,7 +50,7 @@ Feldzuordnung zu den HiveGrid-Entities steht in [`docs/DATENMODELL.md`](docs/DAT
|
||||
|
||||
**Achtung Zeitbezug:** `features/dashboard/utils/dateUtils.ts` definiert
|
||||
`HEUTE` als festen Stichtag (30.07.2026), damit der Prototyp reproduzierbar
|
||||
bleibt. Beim Einbau durch `new Date()` ersetzen — die Konstante wird an
|
||||
bleibt. Beim Einbau durch `new Date()` ersetzen, die Konstante wird an
|
||||
elf Stellen verwendet, alle über den Import.
|
||||
|
||||
### 2.2 Filter serverseitig anwenden
|
||||
@@ -70,7 +70,7 @@ Diese Elemente lösen bewusst nur einen Hinweis-Toast aus:
|
||||
| Teilen | `components/layout/PageHeader.tsx` | Link mit eingefrorenen Filtern |
|
||||
| Aktualisieren | `components/layout/PageHeader.tsx` | Daten neu laden |
|
||||
| „Als Bild exportieren" | `components/cards/ChartCard.tsx` | PNG-Export des Charts |
|
||||
| Einträge in „Handlungsbedarf" | `components/cards/AlertList.tsx` | Absprung in die jeweilige Arbeitsliste — die Handlung je Zeile (`aktion`) beschreibt, was dort passieren soll |
|
||||
| Einträge in „Handlungsbedarf" | `components/cards/AlertList.tsx` | Absprung in die jeweilige Arbeitsliste, die Handlung je Zeile (`aktion`) beschreibt, was dort passieren soll |
|
||||
| Tabellenzeilen | `components/tables/DataTable.tsx` | Absprung auf den Datensatz |
|
||||
| Suche und Glocke (Kopfzeile) | `components/layout/AppShell.tsx` | Attrappe, gehört zur bestehenden Shell |
|
||||
|
||||
@@ -78,7 +78,7 @@ Diese Elemente lösen bewusst nur einen Hinweis-Toast aus:
|
||||
|
||||
- **Zeitraum-Umschaltung** rechnet alle Kennzahlen und Charts neu, inklusive Trendvergleich zur Vorperiode
|
||||
- **Mehrfachfilter** (Gewerk, Status, Quelle, Region, Mitarbeiter) mit entfernbaren Chips
|
||||
- **Seitenabhängige Filterrelevanz** — irrelevante Filter werden gar nicht erst angeboten
|
||||
- **Seitenabhängige Filterrelevanz**: irrelevante Filter werden gar nicht erst angeboten
|
||||
- **Widgets ausblenden** über das ⋯-Menü jeder Karte, Wiedereinblenden über die Filterleiste, persistiert
|
||||
- **Tabellen** sortieren, durchsuchen, blättern
|
||||
- **Hover** auf Balken, Ringsegmenten, Trichterstufen und Verlaufspunkten
|
||||
@@ -102,12 +102,12 @@ bleiben über Reloads und Screenshots identisch, sonst ist keine Abstimmung übe
|
||||
konkrete Werte möglich.
|
||||
|
||||
**Team-Seite enthalten,** obwohl die Planung sie für v2 vorgesehen hatte.
|
||||
Auslastung ist im Handwerk die Kennzahl, nach der disponiert wird — ohne sie
|
||||
Auslastung ist im Handwerk die Kennzahl, nach der disponiert wird, ohne sie
|
||||
fehlt dem Dashboard der Bezug zum Tagesgeschäft.
|
||||
|
||||
**Kein Dark Mode.** Die Carbon-Palette ist hier im Light Theme hinterlegt.
|
||||
Ein Dark Theme braucht die zweite Palettenvariante (in `@carbon/charts`
|
||||
enthalten) und eigene Kontrastprüfung — bewusst nicht angefangen, statt es
|
||||
enthalten) und eigene Kontrastprüfung: bewusst nicht angefangen, statt es
|
||||
halb zu machen.
|
||||
|
||||
---
|
||||
@@ -117,7 +117,7 @@ halb zu machen.
|
||||
1. `data/types.ts` gegen die echten Entity-Typen abgleichen
|
||||
2. `data/seed.ts` durch die echte Datenquelle ersetzen
|
||||
3. `HEUTE` in `utils/dateUtils.ts` auf `new Date()` umstellen
|
||||
4. `view-models/index.ts` gegen `@hivegrid/view-models` tauschen — der Vertrag
|
||||
4. `view-models/index.ts` gegen `@hivegrid/view-models` tauschen, der Vertrag
|
||||
(`VMState`) ist wortgleich, es ist ein Import-Wechsel
|
||||
5. Eine Seite vollständig durchziehen (Übersicht): Loader ruft
|
||||
`baueUebersichtVM()`, die Route rendert `<UebersichtSeite vm={…} />`. Der
|
||||
@@ -133,7 +133,7 @@ halb zu machen.
|
||||
## 5. Offene Punkte für die Fachabstimmung
|
||||
|
||||
- **Auslastung** rechnet produktive Stunden gegen Vertragsstunden. Ob Rüst- und
|
||||
Fahrzeiten als produktiv zählen, ist eine betriebliche Festlegung — die
|
||||
Fahrzeiten als produktiv zählen, ist eine betriebliche Festlegung, die
|
||||
Schwellen (70 % / 95 %) stehen in `components/charts/AuslastungsBalken.tsx`.
|
||||
- **Umsatz** ist hier Zahlungseingang, nicht Rechnungsstellung. Für die
|
||||
betriebswirtschaftliche Auswertung kann die andere Definition gewünscht sein;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# HiveGrid Analytics Dashboard — UI/UX-Prototyp
|
||||
# HiveGrid Analytics Dashboard: UI/UX-Prototyp
|
||||
|
||||
Gestaltungsvorlage für die Analytics-Dashboards in HiveGrid, fachlich auf einen
|
||||
SHK-/Handwerksbetrieb zugeschnitten. **Ziel dieses Repos ist die Übergabe an die
|
||||
Entwicklung** — nicht der Produktivbetrieb.
|
||||
Entwicklung**, nicht der Produktivbetrieb.
|
||||
|
||||
> **Zwei Features, nicht eines.** „Analytics" ist der Analysebereich mit fünf
|
||||
> Seiten und einem seitenweiten Filter; „Dashboard" ist der
|
||||
> Begrüßungsbildschirm — eine Seite aus Kacheln, jede mit eigenem Zeitraum. Es
|
||||
> Begrüßungsbildschirm, eine Seite aus Kacheln, jede mit eigenem Zeitraum. Es
|
||||
> zeigt die wichtigsten Auswertungen **plus** anderes (HR-Anträge, Aufgaben,
|
||||
> Termine), das noch nicht gebaut ist und im Raster als reservierte Plätze
|
||||
> steht. Beide sind gestaltet; offen ist, was aus der Analytics-Übersichtsseite
|
||||
@@ -25,14 +25,14 @@ npm run dev # http://localhost:4300 → leitet auf /dashboard
|
||||
|
||||
| Bereich | Inhalt |
|
||||
|---|---|
|
||||
| **5 Analyse-Seiten** | Übersicht, Finanzen, Aufträge, Kunden, Team — als Reiter im Menüpunkt **Analytics** |
|
||||
| **5 Analyse-Seiten** | Übersicht, Finanzen, Aufträge, Kunden, Team, als Reiter im Menüpunkt **Analytics** |
|
||||
| **Seitenleiste** | spiegelt die von HiveGrid Cloud (dieselbe Komponente, dieselben Gruppen) |
|
||||
| **26 KPIs** | vollständig definiert in [`docs/KPI-KATALOG.md`](docs/KPI-KATALOG.md) |
|
||||
| **9 Chart-Bausteine** | Fläche, Säulen, liegende Balken, Ring, Trichter, Auslastung, Sparkline, Legende, Leerzustand |
|
||||
| **Filtersystem** | Zeitraum + 5 Dimensionen, seitenabhängig relevant, in localStorage persistiert |
|
||||
| **Demo-Datensatz** | deterministisch erzeugt: 420 Kunden, 1.900 Aufträge, ~1.260 Rechnungen, 2,4 Mio. € Jahresumsatz |
|
||||
|
||||
Alles ist deutschsprachig, weil die Oberfläche deutschsprachig ist — inklusive
|
||||
Alles ist deutschsprachig, weil die Oberfläche deutschsprachig ist: inklusive
|
||||
Bezeichner im Code. Das erspart beim Einbau die ständige Übersetzung zwischen
|
||||
`revenue` und „Umsatz".
|
||||
|
||||
@@ -41,7 +41,7 @@ Bezeichner im Code. Das erspart beim Einbau die ständige Übersetzung zwischen
|
||||
## Wo was liegt
|
||||
|
||||
```
|
||||
app/ Next.js App Router — je Seite nur ein Wrapper
|
||||
app/ Next.js App Router: je Seite nur ein Wrapper
|
||||
features/dashboard/
|
||||
routes/ Verdrahtung Store → View-Model → Seite (wird beim
|
||||
Einbau durch den Loader ersetzt)
|
||||
@@ -57,7 +57,7 @@ features/dashboard/
|
||||
data/ types · seed (Dummy-Daten) · calculations · filtering
|
||||
· nutzer (Attrappe des angemeldeten Benutzers)
|
||||
hooks/ useAnalyseDaten, useIstSchmal
|
||||
pages/ die sechs Seiten — reine Komponenten ({ vm })
|
||||
pages/ die sechs Seiten: reine Komponenten ({ vm })
|
||||
utils/ chartTheme (Carbon-Tokens), formatters, dateUtils
|
||||
ui/ Subframe-Komponentenbibliothek (unverändert übernommen)
|
||||
docs/ Design-System, KPI-Katalog, Navigation, Datenmodell,
|
||||
@@ -70,7 +70,7 @@ scripts/verify-layout.mjs misst Textkollisionen über alle Seiten und Breite
|
||||
(jede Kennzahl als reine Funktion) und `features/dashboard/view-models/`
|
||||
(je Seite ein reiner Builder `(daten, filter) → VMState<…>`).
|
||||
|
||||
Die Seiten unter `pages/` sind **reine Komponenten** — sie bekommen ihr
|
||||
Die Seiten unter `pages/` sind **reine Komponenten**, sie bekommen ihr
|
||||
View-Model als Prop und greifen auf keinen Store zu. Das ist genau das Muster,
|
||||
das HiveGrid Cloud verlangt (`<XPage vm={} actions={} />`): Beim Einbau tritt der
|
||||
Loader an die Stelle des Containers unter `routes/`, die Seiten selbst bleiben
|
||||
@@ -82,15 +82,15 @@ unverändert. Details in [`docs/ZIELSYSTEM.md`](docs/ZIELSYSTEM.md) Abschnitt 3.
|
||||
|
||||
Drei Quellen sind hier zusammengeführt:
|
||||
|
||||
1. **Fachliche Planung** (Januar 2026, mit dem Branchenpartner erarbeitet) —
|
||||
1. **Fachliche Planung** (Januar 2026, mit dem Branchenpartner erarbeitet),
|
||||
Seitenschnitt, KPI-Auswahl, Filtersystem, Zahlen- und Spacing-Regeln.
|
||||
2. **Bestehender Pitch-Prototyp** — die Subframe-Komponentenbibliothek unter `ui/`
|
||||
2. **Bestehender Pitch-Prototyp**, die Subframe-Komponentenbibliothek unter `ui/`
|
||||
und die Gestaltungssprache der Oberfläche.
|
||||
3. **IBM Carbon Data Visualization** — Chart-Regeln, Farbpalette, Achsen, Tooltips.
|
||||
3. **IBM Carbon Data Visualization**: Chart-Regeln, Farbpalette, Achsen, Tooltips.
|
||||
Siehe [`docs/DESIGN-SYSTEM.md`](docs/DESIGN-SYSTEM.md).
|
||||
|
||||
Welche Quelle was beigesteuert hat — und an welchen Stellen bewusst von der
|
||||
Januar-Planung abgewichen wurde — steht in [`docs/HERKUNFT.md`](docs/HERKUNFT.md).
|
||||
Welche Quelle was beigesteuert hat, und an welchen Stellen bewusst von der
|
||||
Januar-Planung abgewichen wurde: steht in [`docs/HERKUNFT.md`](docs/HERKUNFT.md).
|
||||
|
||||
---
|
||||
|
||||
@@ -99,7 +99,7 @@ Januar-Planung abgewichen wurde — steht in [`docs/HERKUNFT.md`](docs/HERKUNFT.
|
||||
[`HANDOVER.md`](HANDOVER.md) listet auf, was echte Logik braucht, was bewusst
|
||||
Attrappe ist und in welcher Reihenfolge der Einbau sinnvoll ist.
|
||||
|
||||
Die Seite **/komponenten** zeigt jeden Baustein isoliert mit Dateipfad — der
|
||||
Die Seite **/komponenten** zeigt jeden Baustein isoliert mit Dateipfad, der
|
||||
schnellste Weg, sich zu orientieren.
|
||||
|
||||
```bash
|
||||
@@ -113,10 +113,10 @@ einem Screenshot deshalb unsichtbar bleiben.
|
||||
|
||||
Die Referenzwerte stehen mit Quelle und Version in
|
||||
[`docs/carbon-referenz.json`](docs/carbon-referenz.json), gezogen aus dem
|
||||
ausgelieferten Stylesheet von `@carbon/charts` — nicht aus der Dokumentation,
|
||||
ausgelieferten Stylesheet von `@carbon/charts`, nicht aus der Dokumentation,
|
||||
die an mehreren Stellen davon abweicht.
|
||||
|
||||
Für die Gestaltungsabnahme: [`docs/PRUEFFRAGEN.md`](docs/PRUEFFRAGEN.md) —
|
||||
Für die Gestaltungsabnahme: [`docs/PRUEFFRAGEN.md`](docs/PRUEFFRAGEN.md),
|
||||
Prüffragen mit nachprüfbarem Ja/Nein, für Reviews ohne Designerauge.
|
||||
|
||||
---
|
||||
@@ -124,11 +124,11 @@ Prüffragen mit nachprüfbarem Ja/Nein, für Reviews ohne Designerauge.
|
||||
## Zielsystem
|
||||
|
||||
Eingebaut wird das hier in **HiveGrid Cloud** (`HiveGrid-Cloud`, Gitea
|
||||
`toolctrl-labs`) — die eigentliche Software: pnpm-Monorepo, React Router 7,
|
||||
`toolctrl-labs`), der eigentlichen Software: pnpm-Monorepo, React Router 7,
|
||||
Prisma/Postgres, mandantenfähig. Zielort ist der Tenant-Dashboard-Screen unter
|
||||
`…/<tenant>/dashboard`.
|
||||
|
||||
Dort steht bereits eine Implementierung — bewusst als **Interim-UI**: gebaut aus
|
||||
Dort steht bereits eine Implementierung, bewusst als **Interim-UI**: gebaut aus
|
||||
derselben Subframe-Bibliothek gegen ein stabiles View-Model, damit die
|
||||
Backend-Tickets nicht auf den finalen Screen warten müssen. Die finale Optik ist
|
||||
dort als Nachzug vorgemerkt. Genau die Lücke füllt dieser Prototyp.
|
||||
@@ -137,18 +137,18 @@ Die Ausgangslage ist günstiger als üblich: **dasselbe Subframe-Projekt**
|
||||
(82 der 86 Komponenten identisch benannt) und **dieselbe Chart-Bibliothek**
|
||||
(Recharts). Der Einbau ist ein Port, keine Neuimplementierung.
|
||||
|
||||
Anzupassen sind vor allem vier Dinge — Daten kommen als View-Model-Prop mit fünf
|
||||
Anzupassen sind vor allem vier Dinge: Daten kommen als View-Model-Prop mit fünf
|
||||
Zuständen statt als Import, Texte laufen über i18n, Diagramme brauchen eine
|
||||
Textalternative für Screenreader, und das App-Chrome dieses Prototyps entfällt,
|
||||
weil die Zielanwendung Shell und Sidebar mitbringt.
|
||||
|
||||
**Nicht alles in der Zielanwendung sieht heute final aus.** HiveGrid Cloud
|
||||
arbeitet designer-first: Für jede Phase gibt es einen Brief mit den benötigten
|
||||
UI-Elementen, den finalen Screen entwirft der Designer in Subframe — Layout und
|
||||
UI-Elementen, den finalen Screen entwirft der Designer in Subframe: Layout und
|
||||
Anordnung sind ausdrücklich dessen Entscheidung. Ein großer Teil dieser Screens
|
||||
ist noch offen und läuft bis dahin auf Interim-UI. Für diesen Prototyp heißt
|
||||
das: Verbindlich zu übergeben sind **Kennzahl-Definitionen, Chart-Regeln und
|
||||
Verhalten** — die Pixelanordnung ist ein Vorschlag.
|
||||
Verhalten**, die Pixelanordnung ist ein Vorschlag.
|
||||
|
||||
**Rangfolge:** Wo dieser Prototyp von HiveGrid abweicht, gilt HiveGrid. Schrift,
|
||||
Größen, Abstände, Farben und App-Shell kommen von dort; aus diesem Repo kommen
|
||||
@@ -156,6 +156,6 @@ die Kennzahl-Definitionen, die Diagrammregeln, das Verhalten und die
|
||||
Navigationsstruktur als Verabredung.
|
||||
|
||||
Details, Dateipfade und die konkrete Einbau-Liste stehen in
|
||||
[`docs/ZIELSYSTEM.md`](docs/ZIELSYSTEM.md) — die Rangfolge in Abschnitt 0. Welche Bereiche der Zielanwendung
|
||||
bereits finale Subframe-Screens tragen — und damit als Leitplanke taugen — steht
|
||||
[`docs/ZIELSYSTEM.md`](docs/ZIELSYSTEM.md): die Rangfolge in Abschnitt 0. Welche Bereiche der Zielanwendung
|
||||
bereits finale Subframe-Screens tragen, und damit als Leitplanke taugen: steht
|
||||
in [`docs/LEITPLANKEN.md`](docs/LEITPLANKEN.md).
|
||||
+8
-8
@@ -2,13 +2,13 @@
|
||||
@import "../ui/theme.css";
|
||||
|
||||
/*
|
||||
* Schrift: Instrument Sans, selbst gehostet — identisch zur Software.
|
||||
* Schrift: Instrument Sans, selbst gehostet: identisch zur Software.
|
||||
*
|
||||
* Die Dateien stammen aus `apps/web/public/fonts/` von HiveGrid Cloud, damit
|
||||
* Schriftschnitt und Metrik exakt übereinstimmen. Variable Schrift (400–700).
|
||||
*
|
||||
* Warum das nötig war: `ui/theme.css` verlangt für alle Subframe-Typo-Token
|
||||
* bereits "Instrument Sans", die Schrift wurde aber nie geladen — und der
|
||||
* bereits "Instrument Sans", die Schrift wurde aber nie geladen, und der
|
||||
* Fließtext war zusätzlich auf Inter gesetzt. Ergebnis waren zwei verschiedene
|
||||
* Schriften auf einer Seite, keine davon die der Software.
|
||||
*/
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
/*
|
||||
* Einige Subframe-Komponenten verweisen noch direkt auf Inter. Damit sie nicht
|
||||
* auf die Systemschrift zurückfallen, zeigt der Name auf dieselbe Datei —
|
||||
* auf die Systemschrift zurückfallen, zeigt der Name auf dieselbe Datei,
|
||||
* so hält es die Software auch.
|
||||
*/
|
||||
@font-face {
|
||||
@@ -98,7 +98,7 @@ body {
|
||||
/*
|
||||
* `[tabindex="-1"]` ist ausgenommen: Solche Elemente sind aus der
|
||||
* Tabulatorreihenfolge heraus und bekommen den Fokus ausschliesslich per
|
||||
* Skript — bei uns der Rahmen der Schublade, damit die Sprachausgabe den
|
||||
* Skript, bei uns der Rahmen der Schublade, damit die Sprachausgabe den
|
||||
* Dialog anliest. Ein Ring um den ganzen Rahmen waere dort keine Orientierung,
|
||||
* sondern Laerm; die Orientierung gibt der naechste Tabulatorschritt.
|
||||
*/
|
||||
@@ -117,16 +117,16 @@ body {
|
||||
}
|
||||
|
||||
/*
|
||||
* Nachtrag zu :focus-visible — ein Fall, den der Browser falsch einschaetzt.
|
||||
* Nachtrag zu :focus-visible, ein Fall, den der Browser falsch einschaetzt.
|
||||
*
|
||||
* Setzt ein Skript den Fokus, behandelt Chrome das wie Tastaturbedienung. Beim
|
||||
* Oeffnen und Schliessen der Schublade tun wir genau das (Fokusfalle rein,
|
||||
* Fokus zurueck auf den Hamburger) — nach einem Mausklick stand der Ring
|
||||
* Fokus zurueck auf den Hamburger): nach einem Mausklick stand der Ring
|
||||
* danach sichtbar da, bis man woanders hinklickte.
|
||||
*
|
||||
* `data-eingabe` schreibt AppShell mit: "zeiger" oder "tastatur", je nach
|
||||
* letzter Eingabe. Der Ring entfaellt nur waehrend Zeigerbedienung und nur an
|
||||
* Knoepfen und Links. Eingabefelder sind ausgenommen — dort gehoert der Ring
|
||||
* Knoepfen und Links. Eingabefelder sind ausgenommen, dort gehoert der Ring
|
||||
* zum Zustand "hier wird gerade geschrieben", auch nach einem Klick. Der erste
|
||||
* Tastendruck schaltet ihn ueberall wieder ein.
|
||||
*/
|
||||
@@ -142,7 +142,7 @@ body {
|
||||
* Knopf, der anklickbar ist und nichts Sichtbares bewirkt, weil er nur den
|
||||
* Zustand der unsichtbaren Schublade aendert.
|
||||
*
|
||||
* Einklappen ergibt in einer Schublade ohnehin keinen Sinn — dort oeffnet und
|
||||
* Einklappen ergibt in einer Schublade ohnehin keinen Sinn, dort oeffnet und
|
||||
* schliesst der Hamburger in der Kopfzeile. Also unterhalb `md` ausblenden.
|
||||
* `group/7ce7d712` ist der von Subframe erzeugte Marker des Knopf-Wrappers in
|
||||
* SidebarWithSections; er aendert sich nur bei einem bewussten Resync.
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import "./globals.css";
|
||||
export const metadata: Metadata = {
|
||||
title: "HiveGrid · Analytics Dashboard",
|
||||
description:
|
||||
"UI/UX-Prototyp der Analytics-Dashboards für Handwerksbetriebe (SHK) — Übergabe an die Entwicklung",
|
||||
"UI/UX-Prototyp der Analytics-Dashboards für Handwerksbetriebe (SHK): Übergabe an die Entwicklung",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { redirect } from "next/navigation";
|
||||
import { DASHBOARD_START } from "@/features/dashboard/components/layout/navigation";
|
||||
|
||||
/**
|
||||
* Die Wurzel leitet auf das Dashboard weiter — so wie in der Software, wo
|
||||
* Die Wurzel leitet auf das Dashboard weiter: so wie in der Software, wo
|
||||
* nach der Anmeldung der Begrüßungsbildschirm steht und nicht der
|
||||
* Analysebereich.
|
||||
*
|
||||
|
||||
+12
-12
@@ -10,15 +10,15 @@ Einbau nur die Datenquelle getauscht werden muss.
|
||||
|
||||
| Prototyp | HiveGrid-Entity | Verfügbar | Anmerkung |
|
||||
|---|---|---|---|
|
||||
| `Rechnung` | **`Rechnung`** (Sub-Entity am Projekt) | v1 | Status dort `entwurf/gestellt/bezahlt/storniert` — **kein `ueberfaellig`**. Überfälligkeit wird aus `faellig_am` gegen heute abgeleitet, nicht als Status geführt |
|
||||
| `Rechnung` | **`Rechnung`** (Sub-Entity am Projekt) | v1 | Status dort `entwurf/gestellt/bezahlt/storniert`: **kein `ueberfaellig`**. Überfälligkeit wird aus `faellig_am` gegen heute abgeleitet, nicht als Status geführt |
|
||||
| `Auftrag` | **`Projekt`** in einer Pipeline mit `typ_hint = auftrag` | v1 | „Auftrag" ist nach **AD-20 keine eigene Entität**, sondern eine Pipeline-View auf den Projekt-Kern. Wert = `auftragsvolumen`, Status = `stage_id` und damit **pipeline-intern** |
|
||||
| `Kunde` | **`Kontakt`** mit `kontakt_status = kunde` | v1 | „Kunde" ist ein **Status am Kontakt**, keine Entität. Kontakt ist der Oberbegriff (Privatperson/Organisation); die Verknüpfung zum Projekt läuft über `ProjektKontakt` mit Rolle `auftraggeber` |
|
||||
| `Mitarbeiter` | `TenantUser` + `User` | v1 | |
|
||||
| `Zeiteintrag` | — | **v2** | **Zeiterfassung steht im v2-Backlog.** Damit hat die gesamte Team-Seite in v1 keine Datenquelle — siehe unten |
|
||||
| `Zeiteintrag` |: | **v2** | **Zeiterfassung steht im v2-Backlog.** Damit hat die gesamte Team-Seite in v1 keine Datenquelle: siehe unten |
|
||||
|
||||
> **Die Begriffe des Prototyps sind SHK-Fachsprache, nicht das Datenmodell der
|
||||
> Software.** „Auftrag" und „Kunde" existieren dort nicht als Entitäten. Das ist
|
||||
> kein Fehler des Prototyps — der Betrieb spricht so, und die Oberfläche soll so
|
||||
> kein Fehler des Prototyps, der Betrieb spricht so, und die Oberfläche soll so
|
||||
> sprechen. Beim Einbau ist es aber eine Übersetzungsleistung und keine
|
||||
> Eins-zu-eins-Zuordnung.
|
||||
|
||||
@@ -30,8 +30,8 @@ Auslastung, Produktivquote, erfasste Stunden und Abwesenheiten beruhen alle auf
|
||||
|
||||
1. Team-Seite als v2 einplanen und in v1 nicht ausliefern;
|
||||
2. Stunden aus einem Drittsystem anbinden (der Betrieb erfasst sie ohnehin
|
||||
irgendwo) — dann ist es eine Integrationsfrage, keine Modellfrage;
|
||||
3. Zeiterfassung nach v1 ziehen — die größte Entscheidung von den dreien.
|
||||
irgendwo): dann ist es eine Integrationsfrage, keine Modellfrage;
|
||||
3. Zeiterfassung nach v1 ziehen, die größte Entscheidung von den dreien.
|
||||
|
||||
Das ist eine Produktentscheidung, keine Gestaltungsfrage. Sie gehört vor den
|
||||
Einbau geklärt, weil sie eine von fünf Seiten betrifft.
|
||||
@@ -60,7 +60,7 @@ hat. Ohne dieses Feld müsste der Trichter raten und würde die oberen Stufen
|
||||
systematisch zu klein zeigen.
|
||||
|
||||
`ueberAngebot` trennt das Angebotsgeschäft (ab etwa 6.000 €) von Aufträgen, die
|
||||
direkt beauftragt werden — Wartung, Notdienst, E-Check. Trichter und
|
||||
direkt beauftragt werden: Wartung, Notdienst, E-Check. Trichter und
|
||||
Abschlussquote beziehen sich ausschließlich auf das Angebotsgeschäft.
|
||||
|
||||
**Rechnung:** `entwurf` · `gestellt` · `bezahlt` · `ueberfaellig`.
|
||||
@@ -84,7 +84,7 @@ Lüftungsanlage.
|
||||
|
||||
## Der Demo-Datensatz
|
||||
|
||||
Erzeugt mit festem Seed (`mulberry32`, Seed `20260730`) — die Zahlen bleiben
|
||||
Erzeugt mit festem Seed (`mulberry32`, Seed `20260730`): die Zahlen bleiben
|
||||
über Reloads und Screenshots identisch. Bezugsdatum ist der **30.07.2026**
|
||||
(`HEUTE` in `utils/dateUtils.ts`).
|
||||
|
||||
@@ -104,15 +104,15 @@ Fiktiver Betrieb: **Meister Krause GmbH**, Raum Rhein-Main, 24 Beschäftigte
|
||||
|
||||
**Eingebaute Effekte**, damit die Charts nicht künstlich flach aussehen:
|
||||
|
||||
- **Saisonalität** — Heizung und Lüftung ziehen von September bis Februar an,
|
||||
- **Saisonalität**: Heizung und Lüftung ziehen von September bis Februar an,
|
||||
der Hochsommer ist schwächer (`SAISON_INDEX`)
|
||||
- **Wachstum** — rund 12 % gegenüber dem Vorjahr
|
||||
- **Größenabhängige Abschlussquote** — Wartung und Notdienst werden fast immer
|
||||
- **Wachstum**, rund 12 % gegenüber dem Vorjahr
|
||||
- **Größenabhängige Abschlussquote**: Wartung und Notdienst werden fast immer
|
||||
ausgeführt, große Anlagen dagegen verglichen und häufiger verloren
|
||||
- **Zahlungsverhalten** — die Mehrheit zahlt nahe am Zahlungsziel (21 Tage),
|
||||
- **Zahlungsverhalten**, die Mehrheit zahlt nahe am Zahlungsziel (21 Tage),
|
||||
ein kleiner Rest wird zum Mahnfall; Forderungen älter als sieben Monate sind
|
||||
eingebracht oder ausgebucht und stehen nicht mehr in der offenen Liste
|
||||
- **Förderanträge** — nur bei Vorgängen der letzten rund 100 Tage offen; ein
|
||||
- **Förderanträge**, nur bei Vorgängen der letzten rund 100 Tage offen; ein
|
||||
Bescheid dauert Wochen, nicht Jahre
|
||||
|
||||
Diese Effekte sind fachlich begründet, nicht kosmetisch: Ein gleichverteilter
|
||||
|
||||
+29
-29
@@ -4,11 +4,11 @@ Grundlage ist **IBM Carbon Data Visualization**, ergänzt um die Spacing- und
|
||||
Zahlenregeln aus der eigenen Planung (`UI_UX_GUIDELINES.md`, Januar 2026).
|
||||
|
||||
Die konkreten Farb- und Linienwerte sind aus dem ausgelieferten Stylesheet von
|
||||
`@carbon/charts` (Light Theme) übernommen — nicht aus der Dokumentation
|
||||
`@carbon/charts` (Light Theme) übernommen, nicht aus der Dokumentation
|
||||
abgeschrieben. Alles Folgende ist in
|
||||
`features/dashboard/utils/chartTheme.ts` als Code hinterlegt.
|
||||
|
||||
## 0. Was Carbon regelt — und was nicht
|
||||
## 0. Was Carbon regelt, und was nicht
|
||||
|
||||
Wichtig für jeden, der hier etwas anzweifelt: **Carbon regelt Datenvisualisierung
|
||||
nur teilweise.** Die Grenze verläuft so:
|
||||
@@ -26,14 +26,14 @@ nur teilweise.** Die Grenze verläuft so:
|
||||
Die Carbon-Seiten zu Chart-Anatomie und Dashboards tragen ausdrücklich den
|
||||
Hinweis „this guidance is a work in progress" und enthalten **keine Zahlenwerte**
|
||||
für Abstände. Wer dort nach Paddings sucht, findet Prinzipien („create a clear
|
||||
visual hierarchy", „limit the number of metrics") — richtig, aber nicht
|
||||
visual hierarchy", „limit the number of metrics"): richtig, aber nicht
|
||||
nachprüfbar.
|
||||
|
||||
**Die Abstandswerte in diesem Dokument stammen deshalb nicht von Carbon**,
|
||||
sondern aus zwei anderen Quellen:
|
||||
|
||||
1. der **Carbon-Spacing-Skala und dem 2x-Grid** des allgemeinen Design-Systems
|
||||
(4/8/12/16/24/32/48…) — dieselbe Skala, aus der auch Davids
|
||||
(4/8/12/16/24/32/48…): dieselbe Skala, aus der auch Davids
|
||||
`INTERIM-UI-DESIGN-STANDARDS.md` im Zielsystem schöpft;
|
||||
2. der eigenen Planung mit dem Branchenpartner (`UI_UX_GUIDELINES.md`).
|
||||
|
||||
@@ -41,7 +41,7 @@ Beides ist konsistent zueinander. Es ist aber ehrlicher zu sagen: Bei Farben und
|
||||
Achsen folgen wir einer Norm, bei Abständen einer begründeten Hauskonvention.
|
||||
|
||||
**Nachprüfbar gemacht:** `npm run verify:carbon` gleicht `chartTheme.ts` gegen
|
||||
`docs/carbon-referenz.json` ab — Palette, neutrale Werte, Tooltip, Achsenraster.
|
||||
`docs/carbon-referenz.json` ab: Palette, neutrale Werte, Tooltip, Achsenraster.
|
||||
Die Referenzdatei nennt Quelle, Version und Prüfdatum. Bewusste Abweichungen
|
||||
(Balkenbreite, feste Kategoriefarben) sind dort begründet hinterlegt und werden
|
||||
als Hinweis ausgegeben, nicht als Fehler.
|
||||
@@ -58,7 +58,7 @@ als Hinweis ausgegeben, nicht als Fehler.
|
||||
11 #009d9a 12 #012749 13 #8a3800 14 #a56eff
|
||||
```
|
||||
|
||||
Die Reihenfolge ist auf Unterscheidbarkeit optimiert — auch bei
|
||||
Die Reihenfolge ist auf Unterscheidbarkeit optimiert, auch bei
|
||||
Farbfehlsichtigkeit und in Graustufen. Farben deshalb **immer von vorne
|
||||
vergeben**, nie nach Geschmack auswählen und nie umsortieren.
|
||||
|
||||
@@ -76,7 +76,7 @@ keine Information mehr, sondern Dekoration.
|
||||
| neutral | `#8d8d8d` |
|
||||
| Information | `#1192e8` |
|
||||
|
||||
Kategorie- und Statusfarben nie im selben Chart mischen — eine grüne Kategorie
|
||||
Kategorie- und Statusfarben nie im selben Chart mischen, eine grüne Kategorie
|
||||
neben einem grünen Positiv-Status ist nicht mehr eindeutig lesbar.
|
||||
|
||||
**Rangfolgen einfarbig.** Top-Kunden oder Aufträge je Mitarbeiter sind keine
|
||||
@@ -106,7 +106,7 @@ Aussage der Grafik verwässern.
|
||||
|
||||
Altersklassen, Risikostufen und Ähnliches sind **keine Kategorien**, sondern eine
|
||||
Reihenfolge mit wachsendem Risiko. Dafür gilt die vierstufige Risikorampe
|
||||
(`RISIKO_RAMPE` in `chartTheme.ts`) — durchgehend Statusfarben, keine
|
||||
(`RISIKO_RAMPE` in `chartTheme.ts`): durchgehend Statusfarben, keine
|
||||
Palettenfarbe dazwischen:
|
||||
|
||||
| Stufe | Wert | Bedeutung |
|
||||
@@ -138,7 +138,7 @@ Die Vergleichsreihe ist bewusst neutral: Sie soll einordnen, nicht konkurrieren.
|
||||
| Y-Achsen-Breite | 58 px | Platz für „245 T€" ohne Umbruch |
|
||||
|
||||
Bei mehr als 12 Datenpunkten wird nur jede n-te Beschriftung gezeigt, sonst
|
||||
überlappen sie (`MAX_ACHSENLABELS` in `chartTheme.ts` — eine Konstante für
|
||||
überlappen sie (`MAX_ACHSENLABELS` in `chartTheme.ts`, eine Konstante für
|
||||
alle Diagrammtypen).
|
||||
|
||||
**Achsenwerte sind rund.** Recharts leitet Ticks aus `max/(n-1)` ab und rundet
|
||||
@@ -158,7 +158,7 @@ Einheitlich für alle Charts (`components/charts/ChartTooltip.tsx`), Werte aus
|
||||
`.cds--cc--tooltip` im ausgelieferten Stylesheet:
|
||||
|
||||
- Fläche `#ffffff` (`--cds-layer-02`), Text `#161616` (`--cds-text-primary`)
|
||||
- abgesetzt allein durch den Schatten `0 1px 6px 0 rgba(0,0,0,.2)` — **kein Rahmen**
|
||||
- abgesetzt allein durch den Schatten `0 1px 6px 0 rgba(0,0,0,.2)`: **kein Rahmen**
|
||||
- Label und Reihenname 12 px in `#525252`, Wert fett in Tabellenziffern
|
||||
- kein Doppelpunkt zwischen Name und Wert, maximal 270 px breit
|
||||
- Farbmarker je Reihe, 10 × 10 px
|
||||
@@ -179,10 +179,10 @@ Ein gemeinsamer Tooltip verhindert, dass jedes Chart eigene Regeln entwickelt.
|
||||
| Balkenabstand | mindestens halbe Balkenbreite (`barCategoryGap: 28 %`) |
|
||||
| Balkenecken | 2 px, nur an der Werteseite |
|
||||
| Linienstärke Fläche | 2 px, Verlaufsfüllung 24 % → 2 % |
|
||||
| Interpolation | **linear** — eine Monotone-Kurve erfindet Zwischenwerte zwischen zwei Messpunkten |
|
||||
| Interpolation | **linear**, eine Monotone-Kurve erfindet Zwischenwerte zwischen zwei Messpunkten |
|
||||
| Punkte | nur beim Hovern, Radius 4, weißer Rand |
|
||||
| Ring innen/außen | 62 % / 88 %, 2° Segmentabstand |
|
||||
| Ringmitte | Gesamtwert — sonst muss man die Segmente addieren |
|
||||
| Ringmitte | Gesamtwert, sonst muss man die Segmente addieren |
|
||||
| Ringlegende | rechts, einzeilig, mit Wert und Prozentanteil |
|
||||
|
||||
Ringsegmente starten oben (12 Uhr) mit dem größten Wert und laufen im
|
||||
@@ -190,7 +190,7 @@ Uhrzeigersinn absteigend. Auch „Sonstige" wird nach Größe einsortiert, nicht
|
||||
ans Ende gehängt.
|
||||
|
||||
**Ein Ring braucht ein echtes Ganzes.** Beträgt ein Segment über 80 %, trägt die
|
||||
Grafik nichts mehr bei — dann ist die Bezugsgröße falsch gewählt. Beispiel: Der
|
||||
Grafik nichts mehr bei, dann ist die Bezugsgröße falsch gewählt. Beispiel: Der
|
||||
Rechnungsbestand aus zwei Jahren gegen den heutigen Forderungsstand ergibt
|
||||
92 % „Bezahlt". Richtig ist der offene Bestand allein; dann zeigt der Ring, wie
|
||||
viel davon bereits überfällig ist.
|
||||
@@ -198,13 +198,13 @@ viel davon bereits überfällig ist.
|
||||
### Werte in einem engen Band: Punkt statt Balken
|
||||
|
||||
Auslastung liegt betrieblich zwischen etwa 70 und 100 %. Ein Balken **muss** bei
|
||||
Null beginnen, sonst verzerrt seine Länge — und dann sind 74 % und 96 % optisch
|
||||
Null beginnen, sonst verzerrt seine Länge, und dann sind 74 % und 96 % optisch
|
||||
fast gleich lang. Ein Punkt kodiert über die **Position**, nicht über die Länge,
|
||||
und darf deshalb einen ausgeschnittenen Bereich zeigen.
|
||||
|
||||
Der Zielkorridor liegt als graues Band hinter den Punkten, die 100-%-Marke als
|
||||
beschriftete Linie, darunter eine Achse. Damit erklärt sich die Grafik ohne
|
||||
Ampel-Legende — und tote Legendeneinträge („Luft frei", wenn niemand darunter
|
||||
Ampel-Legende, und tote Legendeneinträge („Luft frei", wenn niemand darunter
|
||||
liegt) entfallen.
|
||||
|
||||
---
|
||||
@@ -239,7 +239,7 @@ zwischen den Karten, zerfällt das Raster optisch.
|
||||
Eine Schriftfamilie: **Instrument Sans**, selbst gehostet aus denselben
|
||||
Schriftdateien wie die Software (`public/fonts/`, variabel 400–700). Vorher lief
|
||||
der Prototyp auf Inter, während `ui/theme.css` für alle Subframe-Typo-Token
|
||||
bereits „Instrument Sans" verlangte — zwei Schriften auf einer Seite, keine
|
||||
bereits „Instrument Sans" verlangte: zwei Schriften auf einer Seite, keine
|
||||
davon die der Software. Zahlen immer in Tabellenziffern
|
||||
(`font-variant-numeric: tabular-nums`, global gesetzt), damit Spalten
|
||||
untereinander stehen.
|
||||
@@ -257,20 +257,20 @@ untereinander stehen.
|
||||
| Datum | `30.07.2026`, kurz `30.07.`, Monat `Jul 26` |
|
||||
|
||||
Punkt als Tausendertrenner, Komma als Dezimaltrenner. Innerhalb einer Ansicht
|
||||
immer dasselbe Format — nicht in einer Zeile `245.000 €` und in der nächsten
|
||||
immer dasselbe Format, nicht in einer Zeile `245.000 €` und in der nächsten
|
||||
`245 T€`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Zustände
|
||||
|
||||
Jedes Chart braucht einen **Leerzustand** — Filter können jede Ansicht leer
|
||||
Jedes Chart braucht einen **Leerzustand**: Filter können jede Ansicht leer
|
||||
laufen lassen. `ChartLeer` zeigt dann eine gestrichelte Fläche mit Hinweistext
|
||||
statt eines leeren Rechtecks.
|
||||
|
||||
Hover-Zustände: Karten erhalten einen stärkeren Schatten, Balken und
|
||||
Ringsegmente werden hervorgehoben (die übrigen auf 35–45 % Deckkraft).
|
||||
Tabellenzeilen bekommen **keinen** Hover, solange sie nicht anklickbar sind —
|
||||
Tabellenzeilen bekommen **keinen** Hover, solange sie nicht anklickbar sind,
|
||||
ein Hover ohne Ziel verspricht eine Interaktion, die es nicht gibt.
|
||||
|
||||
## 9. Textabstände und Umbruch
|
||||
@@ -287,7 +287,7 @@ zusammengehörig, bei 0 px kleben Beschriftung und Wert aneinander.
|
||||
schrumpft auf null, der Text bleibt stehen und läuft in das Nachbarelement.
|
||||
Deshalb trägt jedes Textfeld, das schrumpfen kann, entweder `truncate` (wenn
|
||||
Kürzen vertretbar ist) oder `break-words` (wenn nicht). Dateipfade und lange
|
||||
Einzelwörter brauchen zwingend `break-words` — sie haben keine Leerzeichen zum
|
||||
Einzelwörter brauchen zwingend `break-words`, sie haben keine Leerzeichen zum
|
||||
Umbrechen.
|
||||
|
||||
**Legenden weichen nach unten aus.** Carbon sagt, die Legende gehört rechts
|
||||
@@ -295,7 +295,7 @@ neben das Diagramm, „when space is plentiful". Ist der Platz nicht reichlich,
|
||||
rutscht sie unter das Diagramm, statt Namen und Werte ineinanderlaufen zu
|
||||
lassen. Gesteuert über **Container-Queries** (`@container` / `@md:`), nicht über
|
||||
den Viewport: Dieselbe Karte nimmt mal ein Drittel und mal die halbe
|
||||
Seitenbreite ein — der Viewport weiß davon nichts.
|
||||
Seitenbreite ein, der Viewport weiß davon nichts.
|
||||
|
||||
**Nachprüfbar gemacht:** `npm run verify:layout` lädt alle sechs Seiten in
|
||||
sieben Fensterbreiten (1024–1920) und misst jede Textzeile. Kollisionen unter
|
||||
@@ -303,7 +303,7 @@ sieben Fensterbreiten (1024–1920) und misst jede Textzeile. Kollisionen unter
|
||||
|
||||
Warum das ein eigener Prüfer sein muss: Diese Fehler treten **nur in bestimmten
|
||||
Breiten** auf. Die Ringlegende auf der Finanzseite kollidierte bei 1280 und
|
||||
1440 px und war bei 1600 px unauffällig — auf einem Screenshot also unsichtbar.
|
||||
1440 px und war bei 1600 px unauffällig: auf einem Screenshot also unsichtbar.
|
||||
Auge und Einzelbild reichen dafür nicht.
|
||||
|
||||
## 10. Fokus und Kontrast
|
||||
@@ -324,7 +324,7 @@ Dropdown, Karten-Menü) schließen mit **Escape** und geben den Fokus an den
|
||||
auslösenden Knopf zurück.
|
||||
|
||||
**Kontrast:** `--color-text-neutral-tertiary` liegt auf `neutral-700`
|
||||
(5,15:1 auf Weiß), nicht auf `neutral-600` (3,68:1 — unter WCAG AA). Das
|
||||
(5,15:1 auf Weiß), nicht auf `neutral-600` (3,68:1, unter WCAG AA). Das
|
||||
entspricht dem a11y-Override im Zielsystem. `text-neutral-subtle` ist ein
|
||||
Placeholder-Token und für lesbaren Text nicht geeignet.
|
||||
|
||||
@@ -333,7 +333,7 @@ Placeholder-Token und für lesbaren Text nicht geeignet.
|
||||
## Kartenhöhen in einer Zeile
|
||||
|
||||
**Gestreckt wird nur, wenn die Karten einer Zeile dieselbe Bauart haben.**
|
||||
Zwei Listen, zwei Zeitverläufe, zwei Tabellen — da gibt die eine Karte eine
|
||||
Zwei Listen, zwei Zeitverläufe, zwei Tabellen: da gibt die eine Karte eine
|
||||
Höhe vor, die die andere ohnehin fast erreicht, und der Rest verteilt sich
|
||||
oben und unten je zur Hälfte (`justify-center` im Kartenkörper).
|
||||
|
||||
@@ -352,7 +352,7 @@ bei 1680 px:
|
||||
| Vorgänge nach Status ∣ nach Gewerk (Diagramm ∣ Liste) | 190 px | **358 px** |
|
||||
|
||||
Der Grund steckt in der Liste: Eine Balkenliste hat keine Mindesthöhe. Fünf
|
||||
Kategorien ergeben 280 px, eine einzige 112 px — daneben liegt ein
|
||||
Kategorien ergeben 280 px, eine einzige 112 px: daneben liegt ein
|
||||
Zeitverlauf unverändert bei 427 px, weil seine Diagrammfläche fest ist.
|
||||
|
||||
Diese Zeilen zu strecken hieße, eine Karte mit einem Balken auf 427 px
|
||||
@@ -368,12 +368,12 @@ nach unten.
|
||||
**Auf dem Dashboard ist die Zeile aus Zeitverlauf und Balkenliste trotzdem
|
||||
gestreckt.** Das ist kein Widerspruch, sondern derselbe Maßstab: Der
|
||||
Unterschied entsteht dort gar nicht erst groß, weil das Dashboard keinen
|
||||
Filter hat — jede Kachel zeigt ihren festen Zeitraum, die Balkenliste hat
|
||||
Filter hat, jede Kachel zeigt ihren festen Zeitraum, die Balkenliste hat
|
||||
immer ihre fünf Gewerke, und es bleiben rund 90 px, die sich oben und unten
|
||||
verteilen. In Analytics kann derselbe Unterschied durch einen Filter auf
|
||||
315 px wachsen. Der Filter ist der Unterschied, nicht die Kachel.
|
||||
|
||||
Die Karten sind in allen Fällen dynamisch — nirgends steht eine feste Höhe.
|
||||
Die Karten sind in allen Fällen dynamisch: nirgends steht eine feste Höhe.
|
||||
Kommt mehr Inhalt hinein, wächst die Karte.
|
||||
|
||||
---
|
||||
@@ -381,13 +381,13 @@ Kommt mehr Inhalt hinein, wächst die Karte.
|
||||
## Ansprache
|
||||
|
||||
**Die Anwendung duzt.** „2 Punkte brauchen heute deine Aufmerksamkeit", nicht
|
||||
„Ihre". Das gilt für jeden Text, der den Benutzer anspricht — Begrüßung,
|
||||
„Ihre". Das gilt für jeden Text, der den Benutzer anspricht: Begrüßung,
|
||||
Leerzustände, Hinweise, Fehlermeldungen.
|
||||
|
||||
Halb geduzt liest sich schlechter als durchgehend gesiezt: Sobald zwei Sätze
|
||||
auf derselben Seite unterschiedlich ansprechen, klingt die Anwendung wie von
|
||||
zwei Leuten geschrieben. Beim Einbau gilt im Zweifel die Ansprache von
|
||||
HiveGrid — sie hat nach [ZIELSYSTEM.md](ZIELSYSTEM.md) Abschnitt 0 den Vorrang.
|
||||
HiveGrid, sie hat nach [ZIELSYSTEM.md](ZIELSYSTEM.md) Abschnitt 0 den Vorrang.
|
||||
|
||||
**Namen im Prototyp stehen in eckigen Klammern.** Die Begrüßung zeigt
|
||||
„Guten Tag, [Vorname]" und nicht einen erfundenen Namen. Ein fester Name sieht
|
||||
|
||||
+11
-11
@@ -19,7 +19,7 @@ absichtliche Korrektur.
|
||||
| **IBM Carbon Data Visualization** | Chart-Regeln, Farbpalette, Achsen, Tooltips, Kategorien-Obergrenze | laufend |
|
||||
|
||||
Die ersten beiden liegen als lokale Ordner außerhalb dieses Repos und sind
|
||||
**nicht Teil der Übergabe** — alles fachlich Relevante daraus ist in `docs/`
|
||||
**nicht Teil der Übergabe**: alles fachlich Relevante daraus ist in `docs/`
|
||||
überführt. Die Zuordnung unten ist die Brücke zurück, falls jemand die
|
||||
Originale doch braucht.
|
||||
|
||||
@@ -32,7 +32,7 @@ sind hier eingeflossen:
|
||||
|
||||
| Teil der Planung | Wirkung in diesem Repo |
|
||||
|---|---|
|
||||
| `01_CRM-Analytics-Clickdummy/` — React/Vite-Click-Dummy | Seitenschnitt, Komponentenzuschnitt, Filterarchitektur |
|
||||
| `01_CRM-Analytics-Clickdummy/`: React/Vite-Click-Dummy | Seitenschnitt, Komponentenzuschnitt, Filterarchitektur |
|
||||
| `UI_UX_GUIDELINES.md` | 8-px-Raster, Regel „innen ≤ außen", Zahlenformate, KPI-Karten-Aufbau |
|
||||
| `03_Planung-und-Konzept/Analytics/REQUIREMENTS-analytics.md` | Anforderungs-IDs (INFRA, DASH, FIN, ORD, CUST, COMP, FILT) |
|
||||
|
||||
@@ -45,11 +45,11 @@ Die Anforderungs-IDs stehen an mehreren Stellen als Kommentar im Code
|
||||
- **Fünf Analyse-Seiten** in derselben Reihenfolge: Übersicht, Finanzen,
|
||||
Aufträge, Kunden, Team
|
||||
- **Komponentenzuschnitt**: KPICard, ChartCard, AlertList, DataTable,
|
||||
Filterleiste, Mehrfachauswahl mit Suche — dieselbe Aufteilung, neu geschrieben
|
||||
Filterleiste, Mehrfachauswahl mit Suche: dieselbe Aufteilung, neu geschrieben
|
||||
- **Filterarchitektur**: globaler Filter-State über alle Seiten, in
|
||||
`localStorage` persistiert, aktive Einschränkungen als entfernbare Chips
|
||||
- **Seitenabhängige Filterrelevanz** (FILT-05) als Konzept
|
||||
- **Recharts** als Chart-Bibliothek — dieselbe Wahl wie im Click-Dummy
|
||||
- **Recharts** als Chart-Bibliothek: dieselbe Wahl wie im Click-Dummy
|
||||
- **8-px-Raster und deutsche Zahlenformate** aus `UI_UX_GUIDELINES.md`,
|
||||
siehe [DESIGN-SYSTEM.md](DESIGN-SYSTEM.md) Abschnitt 5 und 7
|
||||
|
||||
@@ -63,7 +63,7 @@ Die Anforderungs-IDs stehen an mehreren Stellen als Kommentar im Code
|
||||
| **Chart-Elemente** | Drop-Shadow statt Rahmen | flach, Trennung über Weißraum und Segmentabstand | Schatten auf Datenflächen erzeugen eine Tiefe, die keine Bedeutung trägt |
|
||||
| **Fachlichkeit** | generisches CRM: Abteilungen, Bewertungen, Kundenkontakte | SHK-Handwerk: Gewerke, Förderung, Auslastung, Angebotsgeschäft | Auftrag dieses Prototyps |
|
||||
| **Bezeichner** | englisch (`revenue`, `orders`) | deutsch (`umsatz`, `auftraege`) | Die Oberfläche ist deutsch. Ein Bruch in der Mitte erzwingt beim Einbau ständiges Übersetzen |
|
||||
| **Datensatz** | 25 Mitarbeiter, 200 Kunden, 800 Aufträge, gleichverteilt | 24, 420, 1.900 — mit Saisonalität, Wachstum, Zahlungsverhalten | An einem gleichverteilten Datensatz lässt sich keine Gestaltung prüfen, siehe [DATENMODELL.md](DATENMODELL.md) |
|
||||
| **Datensatz** | 25 Mitarbeiter, 200 Kunden, 800 Aufträge, gleichverteilt | 24, 420, 1.900: mit Saisonalität, Wachstum, Zahlungsverhalten | An einem gleichverteilten Datensatz lässt sich keine Gestaltung prüfen, siehe [DATENMODELL.md](DATENMODELL.md) |
|
||||
|
||||
### Anforderungen, die hier nicht umgesetzt sind
|
||||
|
||||
@@ -75,7 +75,7 @@ fehlen vier davon bewusst:
|
||||
| INFRA-03 | Filter-Presets speichern und laden | Bedienkomfort ohne Gestaltungsfrage. Gehört in die echte Software ans Benutzerprofil, nicht in die Vorlage |
|
||||
| INFRA-04 (teilweise) | Zeitraum mit benutzerdefiniertem Bereich | Die sechs Presets decken die Abstimmung ab; ein Datumsbereichs-Picker ist eine Umsetzungs-, keine Gestaltungsfrage |
|
||||
| FILT-02 / FILT-04 | Filter für Rechnungsstatus und Betragsbereich | Rechnungsstatus ist auf der Finanzseite bereits als Chart-Dimension sichtbar; ein Betragsfilter verzerrt Summen-KPIs, ohne dass es an der Karte steht |
|
||||
| CUST-03 | KPI „Kontakte gesamt" | Der Prototyp führt keine Kontakt-Entität. In HiveGrid existiert sie — die Karte lässt sich beim Einbau ergänzen |
|
||||
| CUST-03 | KPI „Kontakte gesamt" | Der Prototyp führt keine Kontakt-Entität. In HiveGrid existiert sie, die Karte lässt sich beim Einbau ergänzen |
|
||||
|
||||
Umgekehrt ist **Team** enthalten, obwohl die Planung es unter v2 führt
|
||||
(TEAM-01 bis TEAM-03): Auslastung ist im Handwerk die Kennzahl, nach der
|
||||
@@ -90,10 +90,10 @@ gesamte Oberfläche außerhalb der Diagramme.
|
||||
|
||||
| Übernommen | Wo |
|
||||
|---|---|
|
||||
| Komponentenbibliothek — 86 Komponenten, 4 Layouts | `ui/components/`, `ui/layouts/` |
|
||||
| Komponentenbibliothek: 86 Komponenten, 4 Layouts | `ui/components/`, `ui/layouts/` |
|
||||
| Design-Tokens (Farben, Radien, Schatten, Typo-Skala) | `ui/theme.css`, eingebunden in `app/globals.css` |
|
||||
| Icon-Set (Feather über `@subframe/core`) | überall, aufgelöst in `components/layout/FeatherIcon.tsx` |
|
||||
| Stack | Next.js 16 · React 19 · Tailwind v4 — gleiche Versionen |
|
||||
| Stack | Next.js 16 · React 19 · Tailwind v4: gleiche Versionen |
|
||||
|
||||
`ui/` ist **byteidentisch** mit dem Stand im Pitch-Repo und wurde nicht
|
||||
angefasst. Die Bibliothek wird dort über die Subframe-CLI aus dem Projekt
|
||||
@@ -104,7 +104,7 @@ npx @subframe/cli@latest sync --all
|
||||
```
|
||||
|
||||
Wer die Bibliothek aktualisiert, sollte das im Pitch-Repo tun und den Ordner
|
||||
hierher kopieren — sonst laufen die beiden Stände auseinander.
|
||||
hierher kopieren, sonst laufen die beiden Stände auseinander.
|
||||
|
||||
### Was aus der Bibliothek nicht verwendet wird
|
||||
|
||||
@@ -115,7 +115,7 @@ Carbon-Regeln umzusetzen. Die Diagramme unter
|
||||
`features/dashboard/components/charts/` setzen deshalb direkt auf Recharts auf.
|
||||
|
||||
Das ist die einzige Stelle, an der dieser Prototyp von der Pitch-Bibliothek
|
||||
abweicht — überall sonst gilt sie.
|
||||
abweicht: überall sonst gilt sie.
|
||||
|
||||
---
|
||||
|
||||
@@ -126,7 +126,7 @@ Grundlage aller Diagramm-Entscheidungen. Ausformuliert in
|
||||
`features/dashboard/utils/chartTheme.ts`.
|
||||
|
||||
Die Farb- und Linienwerte sind aus dem ausgelieferten Stylesheet von
|
||||
`@carbon/charts` (`dist/styles.css`, Light Theme) übernommen — nicht aus der
|
||||
`@carbon/charts` (`dist/styles.css`, Light Theme) übernommen, nicht aus der
|
||||
Dokumentation abgeschrieben. Das Paket selbst ist **keine Abhängigkeit** dieses
|
||||
Repos; gezogen wurden nur die Werte.
|
||||
|
||||
|
||||
+12
-12
@@ -5,8 +5,8 @@ Alle Funktionen liegen in `features/dashboard/data/calculations.ts`.
|
||||
|
||||
**Zwei Zeitbezüge sind zu unterscheiden:**
|
||||
|
||||
- **zeitraumbezogen** — reagiert auf die Zeitraum-Umschaltung (Umsatz, neue Kunden)
|
||||
- **stichtagsbezogen** — gilt zum heutigen Tag, unabhängig vom Zeitraum
|
||||
- **zeitraumbezogen**: reagiert auf die Zeitraum-Umschaltung (Umsatz, neue Kunden)
|
||||
- **stichtagsbezogen**: gilt zum heutigen Tag, unabhängig vom Zeitraum
|
||||
(offene Forderungen, aktive Aufträge, Auslastung des Bestands)
|
||||
|
||||
Das ist Absicht: „Offene Forderungen der letzten 7 Tage" wäre keine sinnvolle
|
||||
@@ -14,7 +14,7 @@ Zahl.
|
||||
|
||||
In der Oberfläche steht der Zeitbezug in der **Fußzeile jeder KPI-Karte**
|
||||
(„im gewählten Zeitraum" / „zum Stichtag" / „über alle Zeiträume"). Karten mit
|
||||
Vorperiodenvergleich zeigen dort stattdessen den Trend — die Anatomie bleibt
|
||||
Vorperiodenvergleich zeigen dort stattdessen den Trend, die Anatomie bleibt
|
||||
gleich, damit die Reihe eine gemeinsame Grundlinie hat. Diagrammkarten, die
|
||||
den Zeitraumfilter ignorieren, sagen es im Untertitel.
|
||||
|
||||
@@ -35,7 +35,7 @@ den Zeitraumfilter ignorieren, sagen es im Untertitel.
|
||||
|
||||
**Umsatzentwicklung** (`umsatzVerlauf()`) bildet zusätzlich den
|
||||
Vorjahreszeitraum ab: Jede Zahlung wird um zwölf Monate verschoben in denselben
|
||||
Zeitraster einsortiert. Die Auflösung wechselt automatisch — bis 30 Tage
|
||||
Zeitraster einsortiert. Die Auflösung wechselt automatisch, bis 30 Tage
|
||||
tageweise, darüber monatsweise.
|
||||
|
||||
---
|
||||
@@ -73,7 +73,7 @@ grün nach rot eingefärbt.
|
||||
| Vertriebstrichter | Vorgänge je erreichter Stufe | Stichtag | `vertriebsTrichter()` |
|
||||
| Förderquote BAFA/KfW | Anteil **förderfähiger** Aufträge mit beantragter oder bewilligter Förderung | Stichtag | `foerderQuote()` |
|
||||
|
||||
**Zum Trichter:** Er zeigt ausschließlich das **Angebotsgeschäft** — Vorhaben
|
||||
**Zum Trichter:** Er zeigt ausschließlich das **Angebotsgeschäft**: Vorhaben
|
||||
ab etwa 6.000 €, für die schriftlich angeboten wird (Feld `ueberAngebot`).
|
||||
Wartung, Notdienst und E-Check gehen ohne Angebot direkt in die Ausführung;
|
||||
zählte man sie mit, würde der Trichter unten breiter als oben.
|
||||
@@ -83,12 +83,12 @@ Verlorene Vorgänge zählen bis zu der Stufe, auf der sie abgesprungen sind
|
||||
systematisch zu klein und die Übergangsquoten wertlos.
|
||||
|
||||
**Zur Abschlussquote:** gleiche Bezugsgröße wie der Trichter. Sie entspricht
|
||||
deshalb exakt der Übergangsquote von „Angebot" auf „Beauftragt" — im
|
||||
deshalb exakt der Übergangsquote von „Angebot" auf „Beauftragt": im
|
||||
Demo-Datensatz rund 38 %.
|
||||
|
||||
**Zur Förderquote:** Bezugsgröße sind nur förderfähige Leistungen (Wärmepumpe,
|
||||
PV-Anlage, Batteriespeicher, Lüftungsanlage). Über alle Aufträge gerechnet läge
|
||||
sie im einstelligen Bereich und wäre wertlos — eine Badsanierung war nie
|
||||
sie im einstelligen Bereich und wäre wertlos, eine Badsanierung war nie
|
||||
förderfähig.
|
||||
|
||||
---
|
||||
@@ -97,7 +97,7 @@ förderfähig.
|
||||
|
||||
| KPI | Definition | Bezug | Funktion |
|
||||
|---|---|---|---|
|
||||
| Kunden gesamt | Alle Kundendatensätze | Stichtag | — |
|
||||
| Kunden gesamt | Alle Kundendatensätze | Stichtag |: |
|
||||
| Neue Kunden | Anlagedatum im Zeitraum | Zeitraum | `neueKunden()` |
|
||||
| Wiederkehrende Kunden | Anteil mit mehr als einem Auftrag | Stichtag | `wiederkehrquote()` |
|
||||
| Ø Kundenwert | Bezahlter Umsatz je Kunde mit mindestens einem Auftrag | gesamt | abgeleitet |
|
||||
@@ -106,7 +106,7 @@ förderfähig.
|
||||
| Auffällige Zahlungsmoral | Ø Zahlungsdauer über 30 Tage | Stichtag | `zahlungsauffaellig()` |
|
||||
|
||||
Empfehlung und Bestandskunden zusammen zeigen, wie stark der Betrieb ohne
|
||||
Werbebudget wächst — im Handwerk die aussagekräftigste Quellenkennzahl.
|
||||
Werbebudget wächst, im Handwerk die aussagekräftigste Quellenkennzahl.
|
||||
|
||||
---
|
||||
|
||||
@@ -114,7 +114,7 @@ Werbebudget wächst — im Handwerk die aussagekräftigste Quellenkennzahl.
|
||||
|
||||
| KPI | Definition | Bezug | Funktion |
|
||||
|---|---|---|---|
|
||||
| Im Einsatz | Gewerblich Beschäftigte mit Status „aktiv" | Stichtag | — |
|
||||
| Im Einsatz | Gewerblich Beschäftigte mit Status „aktiv" | Stichtag |: |
|
||||
| Ø Auslastung | Produktive Stunden / verfügbare Stunden (aus Wochenstunden hochgerechnet) | Zeitraum | `teamKennzahlen()` |
|
||||
| Produktivquote | Auftragsbezogene Stunden / erfasste Stunden | Zeitraum | abgeleitet |
|
||||
| Erfasste Stunden | Summe aller Zeiteinträge | Zeitraum | abgeleitet |
|
||||
@@ -126,9 +126,9 @@ Werbebudget wächst — im Handwerk die aussagekräftigste Quellenkennzahl.
|
||||
|
||||
| Bereich | Bedeutung |
|
||||
|---|---|
|
||||
| < 70 % | Luft frei — Kapazität ungenutzt |
|
||||
| < 70 % | Luft frei: Kapazität ungenutzt |
|
||||
| 70–95 % | Zielkorridor |
|
||||
| > 95 % | Ausgebucht — Terminrisiko |
|
||||
| > 95 % | Ausgebucht: Terminrisiko |
|
||||
|
||||
Der Auftragswert je Mitarbeiter ist ausdrücklich **kein Leistungsvergleich**:
|
||||
Ein Wärmepumpen-Auftrag ist um ein Vielfaches größer als eine Wartung. Der
|
||||
|
||||
+31
-31
@@ -1,12 +1,12 @@
|
||||
# Leitplanken aus HiveGrid Cloud
|
||||
|
||||
Welche Bereiche der Zielanwendung bereits Davids finale Subframe-Screens tragen
|
||||
und welche noch auf Interim-UI laufen — und was daraus für dieses Dashboard
|
||||
Welche Bereiche der Zielanwendung bereits Davids finale Subframe-Screens tragen,
|
||||
welche noch auf Interim-UI laufen, und was daraus für dieses Dashboard
|
||||
verbindlich ist.
|
||||
|
||||
Warum die Unterscheidung zählt: Ein nachgezogener Screen ist eine
|
||||
**Design-Entscheidung**, an der man sich ausrichten kann. Eine Interim-UI ist
|
||||
eine **Notlösung des Devs** — sie kopieren hieße, einen Platzhalter zum Standard
|
||||
eine **Notlösung des Devs**, sie kopieren hieße, einen Platzhalter zum Standard
|
||||
zu erklären.
|
||||
|
||||
Stand der Auswertung: 2026-08-29 gegen den lokalen Repo-Stand.
|
||||
@@ -20,62 +20,62 @@ Der Repo pflegt dafür bewusste Marker. In absteigender Verlässlichkeit:
|
||||
| Muster | Bedeutung |
|
||||
|---|---|
|
||||
| `Interim-UI (AD-28)` im Kopfkommentar | Optik ist ausdrücklich vorläufig |
|
||||
| `design-nachzug` | offener Punkt — steht auch in sonst fertigen Screens für einzelne Reste |
|
||||
| `design-nachzug` | offener Punkt: steht auch in sonst fertigen Screens für einzelne Reste |
|
||||
| Kopfkommentar mit Subframe-Screen-ID (`WF-P8 S03`, `S02/S02L/S03/S05`), Page-UUID oder „gegen Davids Export vom …" | nachgezogen, Optik ist abgestimmt |
|
||||
| `@subframe/sync-disable` in `apps/web/app/ui/**` | lokal geänderte Komponente, die der Sync nicht überschreiben darf |
|
||||
|
||||
Ein typischer Interim-Kopf (`projects/PipelineKanbanPage.tsx`):
|
||||
|
||||
> `// R-P8b (P8 configurable Kanban) Interim-UI (AD-28) — pipeline board screen`
|
||||
> `// R-P8b (P8 configurable Kanban) Interim-UI (AD-28): pipeline board screen`
|
||||
> `// (D-KANBAN), composed from ~/ui …`
|
||||
|
||||
Ein typischer nachgezogener Kopf (`projects/ProjektListPage.tsx`):
|
||||
|
||||
> `// WF-P8 S03 "Projekt-Liste" — rebuilt against Davids Subframe export of`
|
||||
> `// WF-P8 S03 "Projekt-Liste": rebuilt against Davids Subframe export of`
|
||||
> `// 2026-07-28 (page 5c5bdb0a-995e-4c22-aa8b-e48333c60297) …`
|
||||
|
||||
**Autoritativ ist aber die Liste, nicht der Kommentar:**
|
||||
`.planning/SUBFRAME-FLOW-INVENTAR.md` trackt jeden Subframe-Flow mit Seitenzahl,
|
||||
Owner und Stand. Der dortige Hinweis ist wichtig: Der Subframe-Sync pflegt nur
|
||||
Components, Layouts und Theme — **Pages werden nie automatisch integriert**,
|
||||
Components, Layouts und Theme: **Pages werden nie automatisch integriert**,
|
||||
jeder Screen wird einzeln hinter der Loader/Action-Grenze verdrahtet.
|
||||
|
||||
**Grenze der Methode:** Ein fehlender Marker sagt nichts. Von 214
|
||||
`*Page.tsx`-Dateien tragen 92 einen Interim-Marker, 36 einen Subframe-Bezug und
|
||||
**86 gar keinen** — überwiegend Plattform-Admin (31) und Integrationen (13),
|
||||
**86 gar keinen**: überwiegend Plattform-Admin (31) und Integrationen (13),
|
||||
also interne Flächen mit niedrigem Politur-Druck.
|
||||
|
||||
---
|
||||
|
||||
## 2. Die Abgrenzung
|
||||
|
||||
### Nachgezogen — als Leitplanke brauchbar
|
||||
### Nachgezogen, als Leitplanke brauchbar
|
||||
|
||||
| Bereich | Stand laut Inventar | Im Code |
|
||||
|---|---|---|
|
||||
| **Team-Chat** | 🟢 24 von 24 Screens integriert (Handoff 2026-08-08) | `screens/chat/` — kein einziger Interim-Marker |
|
||||
| **Form-Builder** | ✅ integriert (PR #71) | `screens/forms/` — Kopf nennt „WF-P16, screens S02/S02L/S03/S05" |
|
||||
| **Automation / Workflow-Editor** | 🟡 voller Delta-Nachzug (Davids „V1 Vollbild minimal") | `screens/automation/` — 6 Screens mit Subframe-Bezug, keiner interim |
|
||||
| **Projekt-Pipelines** | 🟡 Kernstrecke S01–S07 integriert | `screens/projects/` — 7 nachgezogen, 4 noch interim (darunter das Kanban selbst) |
|
||||
| **Aufgaben** | 🟡 Stufen 0–6 fertig | `screens/tasks/` — 2 nachgezogen, 1 interim |
|
||||
| **Lead-Liste** | 🟡 Kernstrecke (`/leads` = S01, `/leads/inbox` = S02) | `screens/leads/` — 2 nachgezogen, 6 noch interim |
|
||||
| **Team-Chat** | 🟢 24 von 24 Screens integriert (Handoff 2026-08-08) | `screens/chat/`, kein einziger Interim-Marker |
|
||||
| **Form-Builder** | ✅ integriert (PR #71) | `screens/forms/`: Kopf nennt „WF-P16, screens S02/S02L/S03/S05" |
|
||||
| **Automation / Workflow-Editor** | 🟡 voller Delta-Nachzug (Davids „V1 Vollbild minimal") | `screens/automation/`: 6 Screens mit Subframe-Bezug, keiner interim |
|
||||
| **Projekt-Pipelines** | 🟡 Kernstrecke S01–S07 integriert | `screens/projects/`: 7 nachgezogen, 4 noch interim (darunter das Kanban selbst) |
|
||||
| **Aufgaben** | 🟡 Stufen 0–6 fertig | `screens/tasks/`: 2 nachgezogen, 1 interim |
|
||||
| **Lead-Liste** | 🟡 Kernstrecke (`/leads` = S01, `/leads/inbox` = S02) | `screens/leads/`: 2 nachgezogen, 6 noch interim |
|
||||
|
||||
### Vollständig Interim — nicht als Vorbild nehmen
|
||||
### Vollständig Interim, nicht als Vorbild nehmen
|
||||
|
||||
Kein einziger nachgezogener Screen: **HR** (21 Seiten), **Mobile-PWA** (19),
|
||||
**CRM** (6), **Booking** (5), **DSGVO/Privacy** (5), **E-Mail-Postfach** (4),
|
||||
**Files** (3), **Kalender** (2), **Audit** (2), Signatur, Plugins, Auth —
|
||||
**Files** (3), **Kalender** (2), **Audit** (2), Signatur, Plugins, Auth,
|
||||
**und das Dashboard** (1).
|
||||
|
||||
### Noch gar nicht entworfen
|
||||
|
||||
Die zehn Auth-Flows (Login/2FA, Passwort-Reset, Set-Password, Admin-RBAC,
|
||||
Signup, Konto löschen, 2FA verwalten, Passwort ändern, Workspace-Auswahl,
|
||||
Profil & Sicherheit) stehen im Inventar auf ❌ offen — zusammen 44 Seiten.
|
||||
Profil & Sicherheit) stehen im Inventar auf ❌ offen: zusammen 44 Seiten.
|
||||
|
||||
Ergänzend: Die 30 Designer-Briefs unter `.planning/designer-briefs/` haben ihren
|
||||
Block „Davids Delta" durchgehend **leer**. Die tatsächlichen Handoffs werden
|
||||
über das Flow-Inventar und die Ticket-Docs geführt, nicht über die Briefs —
|
||||
über das Flow-Inventar und die Ticket-Docs geführt, nicht über die Briefs,
|
||||
102 Ticket-Docs führen einen offenen `design-nachzug`.
|
||||
|
||||
---
|
||||
@@ -100,7 +100,7 @@ Die API ist knapp:
|
||||
|
||||
Unsere `KPICard` kann mehr: Sparkline, Akzentfarbe, `richtungGut` (steigender
|
||||
Wert ist je nach Kennzahl gut oder schlecht), Trendlabel, ⋯-Menü zum Ausblenden.
|
||||
Das ist fachlich begründet — **aber es ist eine Abweichung vom gesetzten
|
||||
Das ist fachlich begründet: **aber es ist eine Abweichung vom gesetzten
|
||||
Baustein.** Beim Einbau gibt es genau zwei saubere Wege:
|
||||
|
||||
1. `StatsCard` erweitern und die Datei mit `@subframe/sync-disable` schützen
|
||||
@@ -118,7 +118,7 @@ mobile:grid-cols-1 grid w-full grid-cols-4 gap-4
|
||||
```
|
||||
|
||||
`gap-4` = 16 px = unser Kartenabstand ✓. Das `mobile:`-Präfix ist der
|
||||
hauseigene Breakpoint und in 44 Stellen belegt — unser Prototyp hat kein
|
||||
hauseigene Breakpoint und in 44 Stellen belegt: unser Prototyp hat kein
|
||||
Mobil-Verhalten und muss es beim Einbau ergänzen.
|
||||
|
||||
### 3.3 Filterleiste: `FilterBar` statt Eigenbau
|
||||
@@ -126,11 +126,11 @@ Mobil-Verhalten und muss es beim Einbau ergänzen.
|
||||
`ProjektListPage`, `AutomationListPage`, `AutomationExecutionsPage`,
|
||||
`FormsAdminPage` und die Betriebsmittel-Übersicht nutzen alle
|
||||
`~/ui/components/FilterBar`. Unsere `AnalyticsToolbar` ist handgebautes
|
||||
Tailwind — funktional gleichwertig, aber am gesetzten Baustein vorbei. Das ist
|
||||
Tailwind: funktional gleichwertig, aber am gesetzten Baustein vorbei. Das ist
|
||||
die auffälligste Stelle, an der der Prototyp beim Einbau umgestellt gehört.
|
||||
|
||||
Bemerkenswert: Die Betriebsmittel-Übersicht wurde 2026-08-19 genau in diese
|
||||
Richtung überarbeitet — „auf kompakte Kennzahlen plus echte `FilterBar`
|
||||
Richtung überarbeitet: „auf kompakte Kennzahlen plus echte `FilterBar`
|
||||
umgestellt". Kennzahlenreihe über Filterleiste über Liste ist dort also bereits
|
||||
das etablierte Muster. Unser Seitenaufbau folgt derselben Logik.
|
||||
|
||||
@@ -139,7 +139,7 @@ das etablierte Muster. Unser Seitenaufbau folgt derselben Logik.
|
||||
- Seitenkopf über `PageShell` / `PageHeader` / `PageBody`, nie eigenes Chrome
|
||||
- i18n mit Namespace: `t("billing:subscriptionOverview.summary.plan")`
|
||||
- Screen-Komponente ist rein: `({ vm, actions })`, kein `useLoaderData`,
|
||||
kein `useFetcher` — Navigation und Schreibpfade sind Callbacks
|
||||
kein `useFetcher`: Navigation und Schreibpfade sind Callbacks
|
||||
- Tabellen über `~/ui/components/Table`, Dichte über die Komponente wählen
|
||||
|
||||
---
|
||||
@@ -148,7 +148,7 @@ das etablierte Muster. Unser Seitenaufbau folgt derselben Logik.
|
||||
|
||||
Recharts kommt im gesamten Repo an **drei** Stellen vor: die beiden
|
||||
Dashboard-Diagramme und `leads/LeadAnalyticsPage.tsx`. Es gibt keine geteilte
|
||||
Palette, kein Chart-Theme, keine Kategorienregel — die vorhandenen Diagramme
|
||||
Palette, kein Chart-Theme, keine Kategorienregel, die vorhandenen Diagramme
|
||||
setzen ihre Farben direkt im JSX.
|
||||
|
||||
Auch Davids Standards decken das nicht ab:
|
||||
@@ -157,12 +157,12 @@ Dialoggrößen und Zustände sehr genau, **Datenvisualisierung gar nicht**.
|
||||
|
||||
Für dieses Dashboard heißt das beides:
|
||||
|
||||
- Für Kacheln, Filter, Tabellen und Seitenaufbau **gibt es** einen Standard —
|
||||
- Für Kacheln, Filter, Tabellen und Seitenaufbau **gibt es** einen Standard,
|
||||
daran halten wir uns, statt ihn neu zu erfinden.
|
||||
- Für Diagramme gibt es keinen. Was wir in
|
||||
[DESIGN-SYSTEM.md](DESIGN-SYSTEM.md) und `chartTheme.ts` festlegen, füllt eine
|
||||
echte Lücke und wird im Zweifel zur Referenz für alle späteren Charts. Das ist
|
||||
der wertvollste, aber auch der begründungspflichtigste Teil der Übergabe —
|
||||
der wertvollste, aber auch der begründungspflichtigste Teil der Übergabe,
|
||||
deshalb steht zu jeder Regel dort, warum sie so ist.
|
||||
|
||||
---
|
||||
@@ -183,7 +183,7 @@ cat .planning/SUBFRAME-FLOW-INVENTAR.md
|
||||
```
|
||||
|
||||
Die Zahlen in diesem Dokument sind eine Momentaufnahme. Das Flow-Inventar wird
|
||||
fortgeschrieben (Stand dort: 2026-08-25, 12. Update) — vor einer Entscheidung
|
||||
fortgeschrieben (Stand dort: 2026-08-25, 12. Update). Vor einer Entscheidung
|
||||
lieber neu zählen.
|
||||
|
||||
---
|
||||
@@ -192,7 +192,7 @@ lieber neu zählen.
|
||||
|
||||
> Vorab, damit die folgende Tabelle nicht falsch gelesen wird: **Eine bewusste
|
||||
> Abweichung ist eine Bitte um eine Entscheidung, kein Freibrief.** Bei
|
||||
> globalen Regeln — Schrift, Größen, Abstände, Farben, Control-Höhen — gilt
|
||||
> globalen Regeln (Schrift, Größen, Abstände, Farben, Control-Höhen) gilt
|
||||
> ohne Diskussion HiveGrid. Die Punkte unten betreffen ausschließlich den
|
||||
> Analytics-Bereich und sind begründet, damit sie beim Einbau nicht
|
||||
> versehentlich „korrigiert" werden. Rangfolge: [ZIELSYSTEM.md](ZIELSYSTEM.md)
|
||||
@@ -205,7 +205,7 @@ als **Entscheidung** erkennbar sind und nicht als Versehen wegkorrigiert werden.
|
||||
| Thema | Cloud | Hier | Begründung |
|
||||
|---|---|---|---|
|
||||
| **Zahlen in Tabellen** | linksbündig; `align-right` nur für die Aktionsspalte | rechtsbündig mit `tabular-nums` | IBM Carbon und die eigene Planung (`UI_UX_GUIDELINES.md` §2) verlangen es: Nur rechtsbündig stehen Tausenderstellen untereinander, und genau darum geht es in einem Analytics-Dashboard. Die Cloud-Tabellen zeigen überwiegend Text, nicht Beträge |
|
||||
| **Filterleiste** | `~/ui` `FilterBar` mit Facetten-Slots | eigene `AnalyticsToolbar` | Der Prototyp braucht ein Zeitraum-Segment-Control, das die FilterBar nicht kennt. Beim Einbau ist das der erste Umbau — die Logik passt fast 1:1 auf `FacetFilterBar` |
|
||||
| **Filterleiste** | `~/ui` `FilterBar` mit Facetten-Slots | eigene `AnalyticsToolbar` | Der Prototyp braucht ein Zeitraum-Segment-Control, das die FilterBar nicht kennt. Beim Einbau ist das der erste Umbau, die Logik passt fast 1:1 auf `FacetFilterBar` |
|
||||
| **Formatierung** | `locale/format.ts`, Prozent erwartet 0–1 | eigene `formatters.ts`, Prozent erwartet 0–100 | Der Prototyp hat keine i18n-Schicht. Beim Einbau ersetzen, aber die **Achsenformatierung** (`achsenFormat`, `nettesRaster`) hat dort kein Gegenstück und sollte mitkommen |
|
||||
|
||||
Umgekehrt übernommen wurden: keine Versalien in Spaltenköpfen, Raster mit 16 px
|
||||
@@ -215,7 +215,7 @@ Visualisierung.
|
||||
**Bereits umgesetzt:** Die Seiten sind reine `({ vm })`-Komponenten mit dem
|
||||
wortgleich gespiegelten `VMState`-Vertrag, die Berechnung liegt in reinen
|
||||
Buildern unter `view-models/`, und die fünf Seitenzustände sind ausgestaltet.
|
||||
Store-Zugriff gibt es ausschließlich in `routes/` — genau der Schicht, die beim
|
||||
Store-Zugriff gibt es ausschließlich in `routes/`, genau der Schicht, die beim
|
||||
Einbau durch den Loader ersetzt wird.
|
||||
|
||||
**Offen für den Einbau** (Portierungsarbeit, keine Gestaltung): Texte über
|
||||
|
||||
+17
-17
@@ -4,12 +4,12 @@
|
||||
|
||||
Die Seitenleiste zeigt **nur unsere beiden Features**. Kontakte, Leads,
|
||||
Pipelines, Postfach, Termine und Einstellungen stehen bereits in der
|
||||
Seitenleiste der Software und werden hier nicht verdoppelt — der Prototyp zeigt,
|
||||
Seitenleiste der Software und werden hier nicht verdoppelt, der Prototyp zeigt,
|
||||
was dazukommt, nicht was schon da ist.
|
||||
|
||||
```
|
||||
HIVEGRID
|
||||
├── Dashboard Begrüßungsbildschirm — Widget-Fläche /dashboard
|
||||
├── Dashboard Begrüßungsbildschirm: Widget-Fläche /dashboard
|
||||
└── Analytics ◄── der Prototyp lebt hier /analytics
|
||||
├─────────────────────
|
||||
└── Komponenten Bausteingalerie des Prototyps /komponenten
|
||||
@@ -19,12 +19,12 @@ Gerendert wird mit derselben Komponente wie in der Software
|
||||
(`ui/components/SidebarWithSections`, aus der Cloud-Fassung portiert) und
|
||||
derselben Schrift (Instrument Sans, selbst gehostet aus denselben Dateien).
|
||||
|
||||
> **Für den Einbau:** Verbindlich ist an dieser Seitenleiste die **Struktur** —
|
||||
> **Für den Einbau:** Verbindlich ist an dieser Seitenleiste die **Struktur**,
|
||||
> zwei Einträge, Analytics mit innerer Leiste. Nicht verbindlich ist ihre
|
||||
> Umsetzung hier. In der Software werden die zwei Einträge in die bestehende
|
||||
> `AppSidebar` eingehängt und die innere Leiste über das dortige
|
||||
> `SidebarIconsLayout` gebaut; unser Nachbau wird nicht mitportiert. Globale
|
||||
> Regeln — Schrift, Größen, Abstände, Farben — kommen ausschließlich aus
|
||||
> Regeln (Schrift, Größen, Abstände, Farben) kommen ausschließlich aus
|
||||
> HiveGrid und werden von hier aus **nicht** überschrieben. Siehe
|
||||
> [ZIELSYSTEM.md](ZIELSYSTEM.md) Abschnitt 0.
|
||||
|
||||
@@ -39,8 +39,8 @@ Analyse-Seiten sind darin über eine **innere Leiste** erreichbar:
|
||||
/analytics/team Team
|
||||
```
|
||||
|
||||
Zwei Navigationsebenen für dieselbe Sache — Unterpunkte in der Seitenleiste
|
||||
*und* eine innere Leiste — würden nur konkurrieren.
|
||||
Zwei Navigationsebenen für dieselbe Sache: Unterpunkte in der Seitenleiste
|
||||
*und* eine innere Leiste: würden nur konkurrieren.
|
||||
|
||||
### Warum innere Leiste und nicht Reiter
|
||||
|
||||
@@ -49,9 +49,9 @@ Beides gibt es im Zielsystem, aber für verschiedene Aufgaben:
|
||||
| | innere Leiste (`SidebarIconsLayout`) | Reiter (`Tabs`) |
|
||||
|---|---|---|
|
||||
| Wofür | Arbeitsbereich mit **Geschwisterseiten** | Abschnitte **innerhalb einer Seite** |
|
||||
| Beispiele | Leads, Termine, HR, Einstellungen — 17 Aufrufstellen | Formular-Editor, Chat-Dateien, Projekt-Teilbereiche, Übergabe-Dialog |
|
||||
| Beispiele | Leads, Termine, HR, Einstellungen: 17 Aufrufstellen | Formular-Editor, Chat-Dateien, Projekt-Teilbereiche, Übergabe-Dialog |
|
||||
|
||||
Analytics mit fünf Analyse-Seiten ist der erste Fall — strukturell dasselbe wie
|
||||
Analytics mit fünf Analyse-Seiten ist der erste Fall: strukturell dasselbe wie
|
||||
Leads mit seinen acht. Drei Gründe geben den Ausschlag:
|
||||
|
||||
1. **Kohärenz.** Wer in der Software von Leads nach Analytics wechselt, findet
|
||||
@@ -59,7 +59,7 @@ Leads mit seinen acht. Drei Gründe geben den Ausschlag:
|
||||
2. **Der Platzeinwand trägt nicht.** Die Leiste ist standardmäßig eine 32 px
|
||||
schmale Icon-Spalte und klappt erst bei Zeigerkontakt oder Tastaturfokus auf
|
||||
Beschriftungen aus (verzögert, damit sie beim Überstreichen nicht
|
||||
aufspringt). Sie kostet also kaum Breite — und die brauchen die
|
||||
aufspringt). Sie kostet also kaum Breite, und die brauchen die
|
||||
Analyse-Seiten mit ihren fünfspaltigen Kennzahlenreihen und breiten Tabellen.
|
||||
3. **Reiter sind dort der Rückfall, nicht die Wahl.** `SidebarIconsLayout`
|
||||
zeigt unterhalb `sm` selbst eine Reiterleiste. Auf dem Schreibtisch Reiter zu
|
||||
@@ -67,23 +67,23 @@ Leads mit seinen acht. Drei Gründe geben den Ausschlag:
|
||||
übernimmt diesen Rückfall mit.
|
||||
|
||||
Ein weiterer Vorteil: Die Leiste wächst. Käme eine sechste Analyse-Seite dazu,
|
||||
ist das eine Zeile mehr — ein Reiterband würde eng.
|
||||
ist das eine Zeile mehr, ein Reiterband würde eng.
|
||||
|
||||
„Dashboard" führt auf den **Begrüßungsbildschirm** unter `/dashboard`. Er hat
|
||||
bewusst **keine innere Leiste und keine Filterleiste**: Er ist eine einzelne
|
||||
Seite aus Kacheln, und jede Kachel trägt ihren eigenen Zeitraum. Das ist der
|
||||
sichtbarste Unterschied zwischen den beiden Menüpunkten — Analytics ist ein
|
||||
sichtbarste Unterschied zwischen den beiden Menüpunkten: Analytics ist ein
|
||||
Bereich, das Dashboard eine Seite. Siehe
|
||||
[SCOPE-ANALYTICS-DASHBOARD.md](SCOPE-ANALYTICS-DASHBOARD.md).
|
||||
|
||||
### Schmale Fenster
|
||||
|
||||
Unterhalb **768 px** — derselbe Breakpoint, den die Software als `MOBILE_BREAKPOINT`
|
||||
führt — fährt die Seitenleiste als Schublade über den Inhalt, statt ihn zu
|
||||
Unterhalb **768 px**: derselbe Breakpoint, den die Software als `MOBILE_BREAKPOINT`
|
||||
führt: fährt die Seitenleiste als Schublade über den Inhalt, statt ihn zu
|
||||
verdrängen. Geöffnet wird sie über einen Hamburger links in der Kopfzeile.
|
||||
|
||||
Das ist **nicht erfunden, sondern abgeglichen**: `SubframeAppShell` im
|
||||
Zielsystem rendert an derselben Stelle denselben Knopf — `FeatherMenu`, 32 × 32,
|
||||
Zielsystem rendert an derselben Stelle denselben Knopf: `FeatherMenu`, 32 × 32,
|
||||
`md:hidden`, links vor dem Breadcrumb. Übernommen wurden auch das Verhalten der
|
||||
Schublade (Abdeckung dahinter, Escape schließt, Fokus wandert beim Öffnen auf
|
||||
den ersten Eintrag und kehrt beim Schließen zum Auslöser zurück) und die
|
||||
@@ -128,14 +128,14 @@ es sich entwickelt? → Woran liegt es?**
|
||||
Auf der **Übersicht** steht der Handlungsbedarf direkt unter der KPI-Reihe und
|
||||
über die volle Breite. Sie ist die Seite der Geschäftsführung; dort ist „was ist
|
||||
zu tun" die zweite Frage, nicht die letzte. Jede Zeile trägt Betrag und Handlung
|
||||
(„Mahnlauf starten", „Nachfassen") — eine Liste ohne Verb ist ein Bericht.
|
||||
(„Mahnlauf starten", „Nachfassen"): eine Liste ohne Verb ist ein Bericht.
|
||||
|
||||
Auf den vier Detailseiten bleibt die Arbeitsliste unten: Dort ist sie die
|
||||
Konsequenz aus dem, was darüber steht.
|
||||
|
||||
**Gleiche Kartenhöhe nur bei gleicher Struktur.** Die KPI-Reihe wird gestreckt,
|
||||
damit die fünf Karten als ein Objekt lesbar sind. Gemischte Reihen (Diagramm
|
||||
mit fester Höhe neben Tabelle) stehen auf `items-start` — Strecken erzeugt dort
|
||||
mit fester Höhe neben Tabelle) stehen auf `items-start`: Strecken erzeugt dort
|
||||
nur Weißraum.
|
||||
|
||||
---
|
||||
@@ -173,5 +173,5 @@ Definiert in `features/dashboard/data/filtering.ts`:
|
||||
| Team | ✓ | ✓ | | | | ✓ |
|
||||
|
||||
Ein Filter, der sichtbar ist aber nichts bewirkt, kostet Vertrauen in die
|
||||
Zahlen. Deshalb werden irrelevante Filter gar nicht erst angeboten — die
|
||||
Zahlen. Deshalb werden irrelevante Filter gar nicht erst angeboten, die
|
||||
Auswahl bleibt aber im Hintergrund erhalten und wirkt beim Seitenwechsel wieder.
|
||||
+8
-8
@@ -1,7 +1,7 @@
|
||||
# Prüffragen für die Gestaltungsabnahme
|
||||
|
||||
Für Reviews ohne Designerauge. Die Fragen sind so gestellt, dass man sie am
|
||||
Bildschirm beantworten kann — jede hat ein nachprüfbares Ja oder Nein, keine
|
||||
Bildschirm beantworten kann, jede hat ein nachprüfbares Ja oder Nein, keine
|
||||
Geschmacksfrage.
|
||||
|
||||
Reihenfolge nach Wirkung: Wer nur zehn Minuten hat, nimmt Abschnitt 1.
|
||||
@@ -33,7 +33,7 @@ Handlungsliste braucht je Zeile einen Betrag und ein Verb.
|
||||
|
||||
| Frage | Wenn nein |
|
||||
|---|---|
|
||||
| Beginnt jede Balkenachse bei null? | Balken kodieren über Länge — abgeschnitten lügen sie. Bei engen Wertebändern Punktdiagramm statt Balken |
|
||||
| Beginnt jede Balkenachse bei null? | Balken kodieren über Länge: abgeschnitten lügen sie. Bei engen Wertebändern Punktdiagramm statt Balken |
|
||||
| Hat jede Achse runde Werte (0/50/100, nicht 0/55/110)? | Der Leser rechnet bei jedem Blick mit |
|
||||
| Trägt eine Achse genau eine Einheit? | „0 €" neben „10 T€" ist ein Einheitenwechsel mitten in der Skala |
|
||||
| Belegt in einem Ring kein Segment mehr als etwa 80 %? | Dann ist die Bezugsgröße falsch gewählt, nicht der Diagrammtyp |
|
||||
@@ -58,7 +58,7 @@ Handlungsliste braucht je Zeile einen Betrag und ein Verb.
|
||||
## 4. Layout, Raster und Zustände
|
||||
|
||||
**Vor jedem Review `npm run verify:layout` laufen lassen.** Der Prüfer findet
|
||||
kollidierenden und überlaufenden Text über sieben Fensterbreiten — das ist die
|
||||
kollidierenden und überlaufenden Text über sieben Fensterbreiten, das ist die
|
||||
eine Fehlerklasse, die man mit dem Auge zuverlässig übersieht, weil sie nur in
|
||||
bestimmten Breiten auftritt.
|
||||
|
||||
@@ -81,11 +81,11 @@ Ringlegenden, Achsenbeschriftungen und Tabellenköpfe achten.
|
||||
|
||||
Kurz und ergiebig, in dieser Reihenfolge:
|
||||
|
||||
1. **FT Visual Vocabulary** — ein Poster, das Diagrammtypen nach Aussageabsicht
|
||||
1. **FT Visual Vocabulary**, ein Poster, das Diagrammtypen nach Aussageabsicht
|
||||
ordnet (Vergleich, Verteilung, Anteil, Veränderung …).
|
||||
Das beste Verhältnis von Aufwand zu Ertrag überhaupt.
|
||||
`github.com/Financial-Times/chart-doctor` → `visual-vocabulary`
|
||||
2. **IBCS — International Business Communication Standards**, die SUCCESS-Regeln.
|
||||
2. **IBCS: International Business Communication Standards**, die SUCCESS-Regeln.
|
||||
Das ist der ausgearbeitete Standard für *Geschäfts*grafik, den Carbon nicht
|
||||
ersetzt: einheitliche Notation, Ist/Plan/Vorjahr, Informationsdichte.
|
||||
Frei unter CC BY-SA auf `ibcs.com`.
|
||||
@@ -95,14 +95,14 @@ Kurz und ergiebig, in dieser Reihenfolge:
|
||||
warum Ringe schwach und Punktdiagramme stark sind.
|
||||
4. **Stephen Few, „Information Dashboard Design"**. Das Standardwerk zur
|
||||
Dashboard-Kritik; die ersten beiden Kapitel reichen für ein geschultes Auge.
|
||||
5. **Datawrapper Academy** (`academy.datawrapper.de`) — kurze, gut belegte
|
||||
5. **Datawrapper Academy** (`academy.datawrapper.de`): kurze, gut belegte
|
||||
Artikel zu einzelnen Fragen („Wann Balken, wann Linien", „Warum keine zwei
|
||||
Y-Achsen").
|
||||
|
||||
Zum Zielsystem passend:
|
||||
|
||||
6. **Carbon Data Visualization** (`carbondesignsystem.com/data-visualization/`)
|
||||
für Farben, Achsen und Diagrammtypen — mit der Einschränkung aus
|
||||
für Farben, Achsen und Diagrammtypen, mit der Einschränkung aus
|
||||
[DESIGN-SYSTEM.md](DESIGN-SYSTEM.md) Abschnitt 0: Abstände regelt Carbon nicht.
|
||||
7. **Carbon Spacing und 2x-Grid** (`carbondesignsystem.com/elements/spacing/`)
|
||||
für genau diese Abstände.
|
||||
@@ -114,4 +114,4 @@ Zum Zielsystem passend:
|
||||
Layout und Anordnung sind im Zielsystem **Entscheidung des Designers**
|
||||
(siehe [LEITPLANKEN.md](LEITPLANKEN.md)). Verbindlich zu übergeben sind
|
||||
Kennzahl-Definitionen, Diagrammregeln und Verhalten. Wer diese Liste im Review
|
||||
benutzt, prüft die Substanz — nicht, ob eine Karte 40 px weiter links sitzt.
|
||||
benutzt, prüft die Substanz, nicht, ob eine Karte 40 px weiter links sitzt.
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
**Stand: 31.08.2026.** Entschieden und gebaut sind die Navigation (5.5) und
|
||||
der erste Aufschlag des Dashboards (Abschnitt 3a). Offen und mit den Kollegen
|
||||
zu klären bleiben 5.1, 5.3 und 5.6 — sie betreffen eine **Doppelung, die
|
||||
zu klären bleiben 5.1, 5.3 und 5.6: sie betreffen eine **Doppelung, die
|
||||
gerade absichtlich im Repo steht** und in Abschnitt 3a benannt ist.
|
||||
|
||||
---
|
||||
|
||||
## 1. Die Unterscheidung
|
||||
|
||||
Bisher wurde dieser Prototyp als „das Dashboard" bezeichnet. Das ist ungenau —
|
||||
Bisher wurde dieser Prototyp als „das Dashboard" bezeichnet. Das ist ungenau,
|
||||
es sind zwei Features mit unterschiedlichem Zweck:
|
||||
|
||||
| | **Analytics** | **Dashboard** |
|
||||
@@ -23,7 +23,7 @@ es sind zwei Features mit unterschiedlichem Zweck:
|
||||
|
||||
**Der entscheidende Unterschied:** Das Dashboard zeigt die wichtigsten Charts
|
||||
aus Analytics, ist aber **nicht darauf beschränkt**. Es ist der Ort für alles,
|
||||
was beim Anmelden zählt — auch für Dinge, die keine Auswertung sind:
|
||||
was beim Anmelden zählt, auch für Dinge, die keine Auswertung sind:
|
||||
„2 neue Anträge", offene Dokumente, Termine heute, zugewiesene Aufgaben.
|
||||
|
||||
Analytics liefert also Bausteine an das Dashboard, deckt es aber nicht ab.
|
||||
@@ -32,7 +32,7 @@ Analytics liefert also Bausteine an das Dashboard, deckt es aber nicht ab.
|
||||
|
||||
## 2. Was die Software dafür bereits vorsieht
|
||||
|
||||
Das ist kein neuer Scope für HiveGrid Cloud — die Architektur ist darauf
|
||||
Das ist kein neuer Scope für HiveGrid Cloud, die Architektur ist darauf
|
||||
ausgelegt. Belegt in `packages/view-models/src/dashboard/dashboard-page.vm.ts`:
|
||||
|
||||
```ts
|
||||
@@ -43,7 +43,7 @@ type DashboardWidgetKind = "hero" | "kpi" | "list" | "trend" | "chart";
|
||||
```
|
||||
|
||||
Die Domäne **`hr`** und die Art **`list`** existieren bereits. „2 neue Anträge"
|
||||
ist damit ein `hr`/`kpi`- oder `hr`/`list`-Widget im Katalog — kein Sonderfall,
|
||||
ist damit ein `hr`/`kpi`- oder `hr`/`list`-Widget im Katalog, kein Sonderfall,
|
||||
sondern der vorgesehene Weg.
|
||||
|
||||
Der Designer-Brief P11 nennt die drei Rollen-Layouts sogar konkret:
|
||||
@@ -61,7 +61,7 @@ dokumentiert; was fehlt, ist die Verbindung zu unserer Analytics-Arbeit.
|
||||
## 3. Was das für diesen Prototyp bedeutet
|
||||
|
||||
**Die fünf Analyse-Seiten sind das Analytics-Feature.** Daran ändert sich
|
||||
nichts — Finanzen, Aufträge, Kunden, Team und die Diagrammbausteine bleiben,
|
||||
nichts: Finanzen, Aufträge, Kunden, Team und die Diagrammbausteine bleiben,
|
||||
wie sie sind.
|
||||
|
||||
**Drei Stellen bekommen einen anderen Zuschnitt:**
|
||||
@@ -74,13 +74,13 @@ wie sie sind.
|
||||
2. **Der Handlungsbedarf ist eigentlich ein Dashboard-Element.**
|
||||
„Was ist zu tun" ist die Frage des Begrüßungsbildschirms, nicht der Analyse.
|
||||
Ich habe ihn in der letzten Runde bewusst nach oben auf die Übersicht
|
||||
gezogen, weil dort die Handlungsebene fehlte — unter der neuen Trennung
|
||||
gezogen, weil dort die Handlungsebene fehlte: unter der neuen Trennung
|
||||
gehört er wahrscheinlich ins Dashboard.
|
||||
|
||||
3. **Charts müssen als Widgets einzeln funktionieren.**
|
||||
Gute Nachricht: Sie tun es bereits. Die Diagrammkomponenten unter
|
||||
`components/charts/` sind reine Komponenten, die ihre Daten als Prop
|
||||
bekommen — sie hängen an keiner Seite. Was **nicht** mitwandert, ist die
|
||||
bekommen, sie hängen an keiner Seite. Was **nicht** mitwandert, ist die
|
||||
Kartenhülle `ChartCard`: Ihr ⋯-Menü und das Ausblenden sind Analytics-Logik;
|
||||
im Dashboard macht das die Widget-Registry.
|
||||
|
||||
@@ -88,14 +88,14 @@ wie sie sind.
|
||||
Filterleiste auf alle Karten gleichzeitig. Im Dashboard trägt jedes Widget
|
||||
seinen eigenen Zeitraum (P11: „Edit = nur Widget-Filter/Zeitraum"). Ein
|
||||
Chart, das aus Analytics ins Dashboard gehoben wird, verliert den globalen
|
||||
Filter und braucht einen eigenen — sonst zeigt es stillschweigend einen
|
||||
Filter und braucht einen eigenen, sonst zeigt es stillschweigend einen
|
||||
anderen Zeitraum als die Nachbarkachel.
|
||||
|
||||
---
|
||||
|
||||
## 3a. Was am 31.08.2026 gebaut wurde
|
||||
|
||||
Das Dashboard existiert unter `/dashboard` — als **erster Aufschlag mit
|
||||
Das Dashboard existiert unter `/dashboard`, als **erster Aufschlag mit
|
||||
Analytics-Inhalten**, aber als Widget-Fläche gebaut, nicht als Analytics-Seite
|
||||
mit anderem Titel.
|
||||
|
||||
@@ -129,11 +129,11 @@ bearbeitbar.
|
||||
*Eine Zeile fehlt der Software dafür:* `dashboardWidgetGridClass` kennt keinen
|
||||
Fall `position.x === 9`, weil bisher keine Rolle vier Kacheln nebeneinander
|
||||
stellt. Unsere Kennzahlenreihe tut das. Ohne die Zeile fließt die vierte
|
||||
Kachel dank `grid-flow-row-dense` trotzdem an die richtige Stelle — nur ohne
|
||||
Kachel dank `grid-flow-row-dense` trotzdem an die richtige Stelle, nur ohne
|
||||
festen Anfang. Vermerkt in `components/dashboard/WidgetRaster.tsx`.
|
||||
|
||||
**Die späteren Bereiche stehen nicht auf dem Dashboard.** Ein
|
||||
Begrüßungsbildschirm ist kein Ort für Ankündigungen — wer sich morgens
|
||||
Begrüßungsbildschirm ist kein Ort für Ankündigungen, wer sich morgens
|
||||
anmeldet, will seine Lage sehen und nicht lesen, was später einmal kommt.
|
||||
|
||||
### Wo die späteren Bereiche stehen
|
||||
@@ -141,13 +141,13 @@ anmeldet, will seine Lage sehen und nicht lesen, was später einmal kommt.
|
||||
Meine Aufgaben, Termine heute und HR-Anträge sind an drei Stellen festgehalten,
|
||||
alle drei für den Einbau gedacht und keine davon auf dem Begrüßungsbildschirm:
|
||||
|
||||
1. **`RESERVIERTE_BEREICHE`** in `view-models/dashboard.vm.ts` — die Kacheln als
|
||||
1. **`RESERVIERTE_BEREICHE`** in `view-models/dashboard.vm.ts`, die Kacheln als
|
||||
Datensatz, mit Titel, Beschreibung, Widget-Domäne und Quell-Feature.
|
||||
2. **Ein markierter Kommentarblock in `pages/DashboardSeite.tsx`** an genau der
|
||||
Stelle im Raster, an die sie gehören — mit den konkreten Werten
|
||||
Stelle im Raster, an die sie gehören, mit den konkreten Werten
|
||||
(`<Widget breite={4}>` mit `start={0}`, `{4}`, `{8}`).
|
||||
3. **Die Bausteingalerie unter `/komponenten`**, Abschnitt „Reservierte
|
||||
Kacheln" — dort sind sie gestaltet zu sehen.
|
||||
Kacheln": dort sind sie gestaltet zu sehen.
|
||||
|
||||
Alle drei sind im Designer-Brief P11 benannt und haben in HiveGrid bereits ihre
|
||||
Widget-Domäne (`tasks`, `schedule`, `hr`). Sie brauchen kein neues Raster, nur
|
||||
@@ -163,9 +163,9 @@ irgendwann fragt jemand, warum die Zahl nie hochzählt.
|
||||
weiterhin auf der Analytics-Übersicht. Das ist Absicht und vorläufig.
|
||||
|
||||
Nach Abschnitt 3.2 gehört er ins Dashboard. Ihn gleichzeitig aus der Übersicht
|
||||
zu entfernen, hätte aber 5.1 mitentschieden — die Frage, ob es die
|
||||
Übersichtsseite überhaupt weiter gibt —, und das ist keine Entscheidung, die
|
||||
beim Bauen nebenbei fällt. Solange 5.1 offen ist, steht er doppelt.
|
||||
zu entfernen, hätte aber 5.1 mitentschieden: die Frage, ob es die
|
||||
Übersichtsseite überhaupt weiter gibt. Das ist keine Entscheidung, die beim
|
||||
Bauen nebenbei fällt. Solange 5.1 offen ist, steht er doppelt.
|
||||
|
||||
**Beim Einbau ist das zu bereinigen, nicht zu übernehmen.** Zwei
|
||||
Einstiegsseiten mit derselben Arbeitsliste sind kein Entwurf, sondern ein
|
||||
@@ -175,7 +175,7 @@ Versehen, das sich als Entwurf tarnt.
|
||||
|
||||
- **Keine Rollenumschaltung.** Gestaltet ist die Sicht der Geschäftsführung.
|
||||
Büro und Monteur entstehen aus denselben Kacheln in anderer Zusammenstellung
|
||||
(P11) — das ist eine Produktentscheidung, keine Gestaltungsfrage.
|
||||
(P11): das ist eine Produktentscheidung, keine Gestaltungsfrage.
|
||||
- **Keinen Bearbeitungsmodus.** Die Software hat ihn (`DashboardEditPanel`,
|
||||
Widget-Katalog, gespeichertes Layout). Der Prototyp bildet ihn nicht nach;
|
||||
er zeigt das Ergebnis, nicht das Werkzeug.
|
||||
@@ -200,7 +200,7 @@ Analytics (ein Menüpunkt, Reiter innen) Dashboard (eigener Menüpunkt)
|
||||
Die Kennzahl-Definitionen aus [KPI-KATALOG.md](KPI-KATALOG.md) und die
|
||||
Diagrammregeln aus [DESIGN-SYSTEM.md](DESIGN-SYSTEM.md) gelten für **beide**
|
||||
Features. Ein Umsatzchart im Dashboard rechnet und aussieht wie dasselbe Chart
|
||||
in Analytics — sonst widersprechen sich zwei Seiten derselben Anwendung.
|
||||
in Analytics, sonst widersprechen sich zwei Seiten derselben Anwendung.
|
||||
|
||||
---
|
||||
|
||||
@@ -214,13 +214,13 @@ Nicht entschieden. Reihenfolge nach Tragweite:
|
||||
Begrüßungscharakter und wird zur reinen Kennzahlen-Zusammenfassung.
|
||||
- *C:* Sie entfällt; Analytics besteht aus vier fachlichen Seiten.
|
||||
|
||||
**5.2 Welche Analytics-Charts werden Dashboard-Widgets? — Vorschlag steht.**
|
||||
**5.2 Welche Analytics-Charts werden Dashboard-Widgets?: Vorschlag steht.**
|
||||
Gebaut sind vier: Zahlungseingang, Umsatz nach Gewerk, Vertriebstrichter,
|
||||
Auslastung. Der Trichter war der Zweifelsfall (er lebt von der Übergangsquote
|
||||
und wird klein unlesbar); er steht deshalb auf sechs von zwölf Spalten und
|
||||
nicht auf vier. Die Auswahl ist ein Vorschlag zur Abnahme, keine Festlegung.
|
||||
|
||||
**5.3 Wo lebt der Handlungsbedarf? — jetzt an beiden Orten.**
|
||||
**5.3 Wo lebt der Handlungsbedarf?, jetzt an beiden Orten.**
|
||||
Er steht seit dem 31.08. auf dem Dashboard und weiterhin auf der
|
||||
Analytics-Übersicht, weil das Entfernen 5.1 mitentschieden hätte. Siehe
|
||||
Abschnitt 3a. Das ist der dringlichste der offenen Punkte, weil er als
|
||||
@@ -230,7 +230,7 @@ einziger sichtbar doppelt im Repo steht.
|
||||
Globaler Filter wie in Analytics oder pro Widget wie in P11? Die Software hat
|
||||
sich für „pro Widget" entschieden; das ist beim Übertragen zu beachten.
|
||||
|
||||
**5.5 Navigation — entschieden am 29.08.2026.**
|
||||
**5.5 Navigation: entschieden am 29.08.2026.**
|
||||
Analytics ist **ein einzelner Menüpunkt** in der Seitenleiste, gleichrangig
|
||||
neben Dashboard. Die fünf Analyse-Seiten sind darin über Reiter erreichbar,
|
||||
nicht als Unterpunkte in der Navigation. Das Dashboard bleibt als eigener
|
||||
@@ -240,7 +240,7 @@ Offen bleibt daran nur 5.1: Wenn die Übersichtsseite zum Dashboard wird,
|
||||
verliert der Analytics-Bereich seinen ersten Reiter.
|
||||
|
||||
**5.6 Team-Seite.**
|
||||
Unabhängig davon offen, weil Zeiterfassung im v2-Backlog steht — siehe
|
||||
Unabhängig davon offen, weil Zeiterfassung im v2-Backlog steht: siehe
|
||||
[DATENMODELL.md](DATENMODELL.md).
|
||||
|
||||
---
|
||||
@@ -248,12 +248,12 @@ Unabhängig davon offen, weil Zeiterfassung im v2-Backlog steht — siehe
|
||||
## 6. Bis zur Klärung
|
||||
|
||||
Die Diagrammbausteine, Kennzahl-Definitionen und Regeln gelten unabhängig
|
||||
davon, wie die beiden Features später geschnitten werden — sie sind von der
|
||||
davon, wie die beiden Features später geschnitten werden, sie sind von der
|
||||
Frage nicht betroffen.
|
||||
|
||||
Betroffen ist genau eine Stelle: die Übersichtsseite. Fällt 5.1 auf „sie wird
|
||||
das Dashboard", dann ist die Arbeit größtenteils getan — der Handlungsbedarf
|
||||
steht schon dort, die Kennzahlen auch —, und die Übersichtsseite entfällt.
|
||||
das Dashboard", dann ist die Arbeit größtenteils getan. Der Handlungsbedarf
|
||||
steht schon dort, die Kennzahlen auch, und die Übersichtsseite entfällt.
|
||||
Fällt sie anders, verliert das Dashboard nichts; die Übersicht behält den
|
||||
Handlungsbedarf und das Dashboard gibt ihn ab.
|
||||
|
||||
|
||||
+36
-36
@@ -11,10 +11,10 @@ Größenordnung: 678 Route-Dateien, davon 575 im Core-Manifest.
|
||||
|
||||
---
|
||||
|
||||
## 0. Rangfolge beim Einbau — HiveGrid gewinnt
|
||||
## 0. Rangfolge beim Einbau: HiveGrid gewinnt
|
||||
|
||||
**Dieser Prototyp ist nachrangig.** Wo er von HiveGrid Cloud abweicht, gilt
|
||||
HiveGrid — ohne Rückfrage, ohne Ausnahme. Das betrifft alles Globale:
|
||||
HiveGrid, ohne Rückfrage, ohne Ausnahme. Das betrifft alles Globale:
|
||||
|
||||
| | Wer bestimmt |
|
||||
|---|---|
|
||||
@@ -29,25 +29,25 @@ Dass dieser Prototyp dieselbe Schrift, dieselbe Seitenleisten-Komponente und
|
||||
dieselben Token benutzt, hat **einen einzigen Zweck**: Er soll neben der
|
||||
Software lesbar sein und nicht wie ein Fremdkörper wirken. Es ist **keine
|
||||
Vorlage, an der sich die Software auszurichten hätte.** Fällt beim Einbau auf,
|
||||
dass hier ein Wert von HiveGrid abweicht, ist der Wert hier falsch — nicht dort.
|
||||
dass hier ein Wert von HiveGrid abweicht, ist der Wert hier falsch, nicht dort.
|
||||
|
||||
### Was aus diesem Repo verbindlich ist
|
||||
|
||||
Nur das Fachliche und das Analytics-Eigene:
|
||||
|
||||
1. **Die Navigationsstruktur als Verabredung** — zwei Einträge in der
|
||||
1. **Die Navigationsstruktur als Verabredung**: zwei Einträge in der
|
||||
Seitenleiste (`Dashboard`, `Analytics`), und Analytics trägt eine **innere
|
||||
Leiste** mit den fünf Analyse-Seiten. Verbindlich ist die *Struktur*, nicht
|
||||
ihre Umsetzung hier: In der Software werden diese zwei Einträge in die
|
||||
bestehende `AppSidebar` eingehängt und die innere Leiste über das dortige
|
||||
`SidebarIconsLayout` gebaut. Unser Nachbau ist die Beschreibung, nicht das
|
||||
Bauteil.
|
||||
2. **Kennzahl-Definitionen** ([KPI-KATALOG.md](KPI-KATALOG.md)) —
|
||||
2. **Kennzahl-Definitionen** ([KPI-KATALOG.md](KPI-KATALOG.md)),
|
||||
was gerechnet wird, mit welchem Zeitbezug.
|
||||
3. **Diagrammregeln** ([DESIGN-SYSTEM.md](DESIGN-SYSTEM.md)) — Palette,
|
||||
3. **Diagrammregeln** ([DESIGN-SYSTEM.md](DESIGN-SYSTEM.md)): Palette,
|
||||
Achsen, Diagrammwahl, Leerzustände. Das ist die Lücke, die HiveGrid heute
|
||||
nicht füllt (siehe [LEITPLANKEN.md](LEITPLANKEN.md) Abschnitt 4).
|
||||
4. **Verhalten** — Filterlogik, Zeitbezüge, Widget-Sichtbarkeit,
|
||||
4. **Verhalten**: Filterlogik, Zeitbezüge, Widget-Sichtbarkeit,
|
||||
Sortier- und Blätterverhalten.
|
||||
|
||||
### Was aus diesem Repo Gerüst ist und wegfällt
|
||||
@@ -60,7 +60,7 @@ Prototyp alleine lauffähig und beurteilbar ist. Beim Einbau übernehmen
|
||||
der Software.
|
||||
|
||||
Das **Kachelraster des Dashboards** ist die Ausnahme in dieser Liste: Es ist
|
||||
kein Gerüst, sondern aus `DashboardPage.tsx` der Software übernommen —
|
||||
kein Gerüst, sondern aus `DashboardPage.tsx` der Software übernommen,
|
||||
dieselben Klassen, dieselbe Ableitung aus `size.w` und `position.x`. Es fällt
|
||||
nicht weg, es trifft dort auf sein Original.
|
||||
|
||||
@@ -88,7 +88,7 @@ Fachlich ist das die Phase **P11** (Modul M20): drei feste Rollen-Layouts
|
||||
Komfort-Delta aus **P30** (Edit-Modus, Drilldown, CSV-/Report-Export,
|
||||
Realtime-Opt-out).
|
||||
|
||||
**Wichtig:** Dieser Stand ist ausdrücklich **Interim-UI nach AD-28** — der Dev
|
||||
**Wichtig:** Dieser Stand ist ausdrücklich **Interim-UI nach AD-28**, der Dev
|
||||
baut aus `~/ui` gegen ein stabiles View-Model, damit das Ticket nicht auf den
|
||||
finalen Screen wartet. Der Kommentarkopf von `DashboardPage.tsx` sagt es
|
||||
wörtlich: *„Final Subframe optics = tracked design-nachzug (D-DASH / VD-P30)."*
|
||||
@@ -98,14 +98,14 @@ kein Umbau gegen den Strom.
|
||||
|
||||
---
|
||||
|
||||
## 2. Gemeinsame Basis — und wo sie auseinanderläuft
|
||||
## 2. Gemeinsame Basis, und wo sie auseinanderläuft
|
||||
|
||||
| | Prototyp | HiveGrid Cloud |
|
||||
|---|---|---|
|
||||
| Subframe-Projekt | `f023064a5ca0` | `f023064a5ca0` — **dasselbe** |
|
||||
| Subframe-Projekt | `f023064a5ca0` | `f023064a5ca0`: **dasselbe** |
|
||||
| Bibliothek | `ui/`, 86 Komponenten | `apps/web/app/ui/`, Alias `~/ui/*`, 92 Komponenten |
|
||||
| Überschneidung | 82 Komponenten gleich benannt | dieselben 82 |
|
||||
| Diagramme | Recharts ^2.15.4 | Recharts ^2.15.3 — **dieselbe Bibliothek** |
|
||||
| Diagramme | Recharts ^2.15.4 | Recharts ^2.15.3: **dieselbe Bibliothek** |
|
||||
| Framework | Next.js 16 | React Router 7 |
|
||||
|
||||
Die vier Namensunterschiede: `DialogFullscreen` → `FullscreenDialog`,
|
||||
@@ -120,7 +120,7 @@ Grund, warum der Einbau realistisch ein Port ist und keine Neuimplementierung.
|
||||
|
||||
## 3. Was beim Einbau konkret anders wird
|
||||
|
||||
### 3.1 Daten kommen als View-Model-Prop — bereits umgesetzt
|
||||
### 3.1 Daten kommen als View-Model-Prop: bereits umgesetzt
|
||||
|
||||
**Das ist im Prototyp schon so gebaut.** Jede Analyse-Seite ist eine reine
|
||||
Komponente:
|
||||
@@ -177,13 +177,13 @@ Fünf Zustände statt einem. Der Prototyp kennt nur „Daten da"; `loading`,
|
||||
Was der Builder noch **nicht** kennt: `DashboardPageData` führt in der Cloud
|
||||
zusätzlich `activeRole`, `layout`, `generatedAt`, `streamUrl`, `canCustomize`,
|
||||
`liveUpdatesEnabled`, `layoutSource` und `catalog`. Das Dashboard ist dort
|
||||
rollen- und berechtigungsabhängig — diese Felder kommen beim Einbau dazu.
|
||||
rollen- und berechtigungsabhängig, diese Felder kommen beim Einbau dazu.
|
||||
|
||||
### 3.1a Warum der Prototyp auf Next.js bleibt
|
||||
|
||||
Die Frage liegt nahe, weil die Software auf React Router 7 läuft. Ein Umbau
|
||||
brächte aber nichts: Im gesamten Prototyp gibt es **vier** Next-spezifische
|
||||
Importe, alle in zwei Dateien —
|
||||
Importe, alle in zwei Dateien,
|
||||
|
||||
| Datei | Import | Schicksal beim Einbau |
|
||||
|---|---|---|
|
||||
@@ -191,19 +191,19 @@ Importe, alle in zwei Dateien —
|
||||
| `app/layout.tsx` | `next`, `next/font/google` | entfällt, Root-Layout gehört der Cloud |
|
||||
|
||||
Portiert wird nicht die Anwendung, sondern der Ordner
|
||||
`features/dashboard/{pages,components,view-models,utils}` — und der enthält
|
||||
`features/dashboard/{pages,components,view-models,utils}`, und der enthält
|
||||
keine Framework-Bindung. React 19, Tailwind v4 und Recharts sind in beiden
|
||||
Repos dieselben. Ein Framework-Wechsel im Prototyp wäre Aufwand ohne Ertrag und
|
||||
würde die Gestaltungsarbeit nur verzögern.
|
||||
|
||||
### 3.1b Wo die Analyse in der Navigation landet — offen
|
||||
### 3.1b Wo die Analyse in der Navigation landet: offen
|
||||
|
||||
**Die Seitenleiste des Prototyps ist Gerüst, kein Vorschlag.** Sie existiert nur,
|
||||
damit die Seiten einzeln aufrufbar sind, und entfällt beim Einbau (siehe 3.2).
|
||||
Wohin die fünf Analyse-Seiten in der echten Navigation gehören, ist **noch nicht
|
||||
entschieden**.
|
||||
|
||||
Der Stand in der Software: Die Seitenleiste hat sechs Gruppen —
|
||||
Der Stand in der Software: Die Seitenleiste hat sechs Gruppen,
|
||||
Start (Dashboard, Aufgaben) · Kunden · Projekte · Kommunikation · Organisation ·
|
||||
Automatisierung. **Eine Analyse-Gruppe gibt es nicht.** Es gibt aber bereits
|
||||
zwei konkurrierende Muster:
|
||||
@@ -211,7 +211,7 @@ zwei konkurrierende Muster:
|
||||
| Muster | Beispiel | Charakter |
|
||||
|---|---|---|
|
||||
| Rollen-Dashboard als Startseite | `/dashboard` (P11/P30) | Widgets, drei Rollen-Layouts, „wie ist meine Lage" |
|
||||
| Analyse **innerhalb** der Fachdomäne | `leads/analytics` — existiert bereits | vertieft genau eine Domäne |
|
||||
| Analyse **innerhalb** der Fachdomäne | `leads/analytics`: existiert bereits | vertieft genau eine Domäne |
|
||||
|
||||
Dieser Prototyp schlägt implizit ein drittes Muster vor: einen
|
||||
**domänenübergreifenden Analysebereich** mit gemeinsamem Zeitraum- und
|
||||
@@ -225,7 +225,7 @@ Dimensionsfilter.
|
||||
(Übersicht → Detail). Finanzen greift über Projekte, Rechnungen und Kontakte
|
||||
hinweg und passt in keine einzelne Domäne.
|
||||
*Dagegen:* eine siebte Gruppe in einer schon langen Seitenleiste.
|
||||
2. **Verteilt in die Domänen** — Finanzen unter Projekte, Kunden-Analyse unter
|
||||
2. **Verteilt in die Domänen**: Finanzen unter Projekte, Kunden-Analyse unter
|
||||
Kunden, analog zu `leads/analytics`.
|
||||
*Dafür:* folgt dem bestehenden Muster, keine neue Gruppe.
|
||||
*Dagegen:* Der gemeinsame Zeitraumfilter zerfällt, die Übersichtsseite hat
|
||||
@@ -235,12 +235,12 @@ Dimensionsfilter.
|
||||
Analyse „warum ist das so". Beides in eine Seite zu legen, macht aus dem
|
||||
Einstieg eine Halde.
|
||||
|
||||
**Entschieden am 29.08.2026: Option 1**, mit dem Namen **„Analytics"** — der
|
||||
**Entschieden am 29.08.2026: Option 1**, mit dem Namen **„Analytics"**, der
|
||||
Prototyp setzt das um. Die Seitenleiste spiegelt jetzt die der Software
|
||||
(dieselbe Komponente, dieselben Gruppen); „Analytics" ist ein einzelner
|
||||
Menüpunkt neben „Dashboard", die fünf Seiten sind darin über Reiter erreichbar.
|
||||
|
||||
*Ursprüngliche Empfehlung war der Name* **„Statistiken"** — so heißt der
|
||||
*Ursprüngliche Empfehlung war der Name* **„Statistiken"**: so heißt der
|
||||
Breadcrumb im Prototyp bereits, und „Analyse" ist im Haus durch
|
||||
`leads/analytics` belegt. Das bestehende `/dashboard` bleibt Startseite; unsere
|
||||
Seite „Übersicht" braucht dann einen anderen Namen, sonst konkurrieren zwei
|
||||
@@ -251,14 +251,14 @@ Einstiegsseiten um dieselbe Rolle.
|
||||
| Prototyp | In der Software |
|
||||
|---|---|
|
||||
| „Übersicht" | kollidiert mit der Startseite „Dashboard" |
|
||||
| „Aufträge" | keine Entität — eine Pipeline-View auf den Projekt-Kern (AD-20) |
|
||||
| „Kunden" | keine Entität — ein Status am `Kontakt` |
|
||||
| „Aufträge" | keine Entität, eine Pipeline-View auf den Projekt-Kern (AD-20) |
|
||||
| „Kunden" | keine Entität, ein Status am `Kontakt` |
|
||||
| „Team" | thematisch unter Organisation/HR; Datenquelle erst in v2 |
|
||||
|
||||
Das ist eine Produkt- und Designentscheidung, keine Umsetzungsfrage. Sie gehört
|
||||
in die Runde mit dem Head of Design, nicht in den Einbau.
|
||||
|
||||
**Nachtrag 29.08.2026:** Die Frage hängt an einer größeren — „Analytics" und
|
||||
**Nachtrag 29.08.2026:** Die Frage hängt an einer größeren: „Analytics" und
|
||||
„Dashboard" sind zwei Features. Siehe
|
||||
[SCOPE-ANALYTICS-DASHBOARD.md](SCOPE-ANALYTICS-DASHBOARD.md).
|
||||
|
||||
@@ -267,7 +267,7 @@ in die Runde mit dem Head of Design, nicht in den Einbau.
|
||||
`AppShell.tsx` aus diesem Prototyp fällt beim Einbau **weg**. Die Cloud-Routen
|
||||
laufen bereits in der echten App-Shell mit Sidebar, Tenant-Layout und
|
||||
Breadcrumb (`SubframeAppShell.tsx`, `AppSidebar.tsx`). Screen-Komponenten
|
||||
dürfen dort ausdrücklich **kein** `~/ui/layouts/DefaultPageLayout` rendern —
|
||||
dürfen dort ausdrücklich **kein** `~/ui/layouts/DefaultPageLayout` rendern,
|
||||
das bringt eine eigene Beispiel-Sidebar mit und erzeugt eine doppelte
|
||||
Navigation. Stattdessen gilt `PageShell` / `PageHeader` / `PageBody` aus
|
||||
`components/screens/shared/PageShell.tsx`.
|
||||
@@ -278,7 +278,7 @@ Unsere Seitenköpfe und die Filterleiste bleiben, die Rahmen-Navigation nicht.
|
||||
|
||||
Alle statischen Texte laufen über `react-i18next` (`t("app.dashboard.…")`).
|
||||
Dieser Prototyp hat deutsche Strings fest im JSX. Das ist beim Einbau
|
||||
mechanische Arbeit, aber flächendeckend — es betrifft jede Karte, jede
|
||||
mechanische Arbeit, aber flächendeckend, es betrifft jede Karte, jede
|
||||
Achsenbeschriftung und jeden Leerzustand.
|
||||
|
||||
Die deutschen **Bezeichner im Code** (`umsatz`, `auftraege`) bleiben davon
|
||||
@@ -289,7 +289,7 @@ in P30).
|
||||
|
||||
In der Cloud sind Charts konsequent `aria-hidden="true"`, und die umgebende
|
||||
Karte liefert eine Textzusammenfassung plus Datentabelle für Screenreader.
|
||||
Dieser Prototyp hat das **nicht**. Beim Einbau ist es Pflicht — das Repo fährt
|
||||
Dieser Prototyp hat das **nicht**. Beim Einbau ist es Pflicht, das Repo fährt
|
||||
ein a11y-Lint-Baseline-Gate und axe-Tests je Primitive.
|
||||
|
||||
### 3.5 Stabile Oberflächen-IDs
|
||||
@@ -304,7 +304,7 @@ müssen aber auf das dortige Namensschema umgestellt werden.
|
||||
`.planning/INTERIM-UI-DESIGN-STANDARDS.md` schreibt fest: Page-Padding **24**,
|
||||
Card **16**, Grid-Gap **16**, Section-Gap **24**, Field-Gap **16**,
|
||||
Control-Höhe Medium **36**, Input-Radius **4**. Das deckt sich mit dem
|
||||
8-px-System in [DESIGN-SYSTEM.md](DESIGN-SYSTEM.md) Abschnitt 5 — hier ist
|
||||
8-px-System in [DESIGN-SYSTEM.md](DESIGN-SYSTEM.md) Abschnitt 5: hier ist
|
||||
nichts anzupassen.
|
||||
|
||||
---
|
||||
@@ -318,7 +318,7 @@ Farben direkt im JSX (`fill="#334155"`, `fill="#cbd5e1"`), jedes mit eigenem
|
||||
Gitter (`strokeDasharray="3 3"`), eigener Achsenbreite und eigenem
|
||||
Balkenradius. Es gibt keine geteilte Palette, keine Kategorien-Obergrenze und
|
||||
keine feste Farbzuordnung. `features/dashboard/utils/chartTheme.ts` ist genau
|
||||
das, was dort fehlt — und der Teil, der sich am ehesten unabhängig vom Rest
|
||||
das, was dort fehlt, und der Teil, der sich am ehesten unabhängig vom Rest
|
||||
übernehmen lässt.
|
||||
|
||||
**Ausformulierte Diagramm-Regeln.** [DESIGN-SYSTEM.md](DESIGN-SYSTEM.md) hat in
|
||||
@@ -332,20 +332,20 @@ Definition.
|
||||
|
||||
---
|
||||
|
||||
## 5. Wer entwirft was — der Rest ist noch nicht fertig designt
|
||||
## 5. Wer entwirft was, der Rest ist noch nicht fertig designt
|
||||
|
||||
HiveGrid Cloud ist nach dem **Designer-First-Prinzip** organisiert
|
||||
(`.planning/DESIGNER-FIRST-PLAYBOOK.md`). Der Ablauf ist verbindlich:
|
||||
|
||||
1. Dev/Planung stellt je Phase einen **Designer-Brief** bereit —
|
||||
1. Dev/Planung stellt je Phase einen **Designer-Brief** bereit,
|
||||
*was* das Modul tut und *welche UI-Elemente* vorkommen müssen, **kein Layout**.
|
||||
2. Der Designer entwirft die Screens in Subframe und darf Spec und UI/UX
|
||||
**überschreiben** — Anordnung und Layout sind eine Design-Entscheidung.
|
||||
**überschreiben**: Anordnung und Layout sind eine Design-Entscheidung.
|
||||
3. Das **Delta** (geänderte Spec-Punkte, neue UX-Entscheidungen, Subframe-Link)
|
||||
wird in den Brief eingetragen.
|
||||
4. Der Dev setzt um und zieht Spec und Doku nach.
|
||||
|
||||
Es gibt 31 solcher Briefs (`.planning/designer-briefs/P01…P34.md`) — Auth, CRM,
|
||||
Es gibt 31 solcher Briefs (`.planning/designer-briefs/P01…P34.md`): Auth, CRM,
|
||||
Lead-Inbox, Projekt-Pipelines, Automation-Flow-Editor, Form-Builder, Plantafel,
|
||||
Kalender, Telefonie, HR, Billing, Customer-Portal, Help-Center. Für einen
|
||||
großen Teil davon steht der Delta-Block **noch leer**: Der finale Screen ist
|
||||
@@ -357,7 +357,7 @@ noch nicht entworfen, die Route läuft bis dahin auf Interim-UI.
|
||||
Anwendung. Es fügt sich in eine App-Shell und ein Design-System ein, die
|
||||
bereits stehen.
|
||||
- Was hier an Anordnung und Raster festgelegt ist, ist ein **Vorschlag**. Die
|
||||
Layout-Hoheit liegt beim Designer — der Brief zu P11 sagt ausdrücklich
|
||||
Layout-Hoheit liegt beim Designer, der Brief zu P11 sagt ausdrücklich
|
||||
„Anordnung = Davids freie Hand". Verbindlich zu übergeben sind die
|
||||
**Kennzahl-Definitionen, die Chart-Regeln und das Verhalten**, nicht die
|
||||
Pixelanordnung.
|
||||
@@ -378,7 +378,7 @@ pnpm dev:local
|
||||
```
|
||||
|
||||
Tenant-App auf `http://app.localhost:3000`. Die lokalen Demo-Zugänge (Tenant
|
||||
„HiveGrid Demo SHK") stehen im README von HiveGrid Cloud unter „App starten" —
|
||||
„HiveGrid Demo SHK") stehen im README von HiveGrid Cloud unter „App starten",
|
||||
sie werden hier bewusst nicht wiederholt. Die Demo-Daten sind zunächst
|
||||
ausgeblendet und erscheinen über **„Demo-Daten anzeigen"** oder `?demo=show`.
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ export function AlertList({
|
||||
/**
|
||||
* Der Trennstrich sitzt oben, nicht unten.
|
||||
*
|
||||
* Unten liegend braucht er eine Ausnahme für die letzte Zeile — und
|
||||
* Unten liegend braucht er eine Ausnahme für die letzte Zeile, und
|
||||
* „die letzte Zeile" gibt es bei zwei Spalten zweimal, einmal je
|
||||
* Spalte, an einer Stelle, die von der Anzahl der Einträge abhängt.
|
||||
* Die rechte Spalte behielt dadurch einen Strich, unter dem nichts
|
||||
@@ -82,10 +82,10 @@ export function AlertList({
|
||||
>
|
||||
<button
|
||||
onClick={() =>
|
||||
zeigeToast(eintrag.aktion, `${eintrag.text} — Absprung folgt in der Umsetzung`)
|
||||
zeigeToast(eintrag.aktion, `${eintrag.text}: Absprung folgt in der Umsetzung`)
|
||||
}
|
||||
/* -mx-2 und die zusätzliche Breite: Der Untergrund lief vorher
|
||||
genau bis zum Pfeil und schnitt dort hart ab — bei zwei Spalten
|
||||
genau bis zum Pfeil und schnitt dort hart ab, bei zwei Spalten
|
||||
besonders sichtbar, weil daneben 32 px Spaltenabstand ungefärbt
|
||||
blieben. Jetzt greift er acht Pixel über den Inhalt hinaus und
|
||||
hat runde Ecken; er liest sich als Fläche unter der Zeile statt
|
||||
|
||||
@@ -27,7 +27,7 @@ export function ChartCard({
|
||||
children,
|
||||
className = "",
|
||||
}: {
|
||||
/** Stabile ID — Schlüssel für Ausblenden/Einblenden. */
|
||||
/** Stabile ID: Schlüssel für Ausblenden/Einblenden. */
|
||||
id: string;
|
||||
titel: string;
|
||||
untertitel?: string;
|
||||
@@ -49,7 +49,7 @@ export function ChartCard({
|
||||
const schliesse = (event: MouseEvent) => {
|
||||
if (!menu.current?.contains(event.target as Node)) setMenuOffen(false);
|
||||
};
|
||||
// Escape schliesst und gibt den Fokus an den Ausloeser zurueck — sonst
|
||||
// Escape schliesst und gibt den Fokus an den Ausloeser zurueck, sonst
|
||||
// landet die Tastaturbedienung nach dem Schliessen am Seitenanfang.
|
||||
const taste = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
@@ -123,7 +123,7 @@ export function ChartCard({
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
zeigeToast("Als Bild vorgemerkt", `${titel} — Export folgt in der Umsetzung`);
|
||||
zeigeToast("Als Bild vorgemerkt", `${titel}: Export folgt in der Umsetzung`);
|
||||
setMenuOffen(false);
|
||||
}}
|
||||
className="flex items-center gap-2 px-3 py-2 text-left text-[13px] text-text-neutral-primary hover:bg-neutral-50"
|
||||
@@ -141,7 +141,7 @@ export function ChartCard({
|
||||
{/* justify-center: Karten einer Zeile sind gleich hoch, aber nicht gleich
|
||||
voll. Eine Karte mit einem Balken neben einer mit einem Diagramm über
|
||||
400 px bekommt den Überschuss oben und unten je zur Hälfte, statt ihn
|
||||
unten abzuladen. Der Inhalt behält seine natürliche Höhe — die Karte
|
||||
unten abzuladen. Der Inhalt behält seine natürliche Höhe, die Karte
|
||||
wächst weiter mit, wenn mehr hineinkommt. */}
|
||||
<div className="flex min-h-0 grow flex-col justify-center px-4 pb-4">{children}</div>
|
||||
</section>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { FeatherIcon } from "../layout/FeatherIcon";
|
||||
import { Sparkline } from "../charts/Sparkline";
|
||||
import { CHART, STATUS_FARBEN } from "../../utils/chartTheme";
|
||||
|
||||
/** Zeitbezug einer Kennzahl — siehe docs/KPI-KATALOG.md. */
|
||||
/** Zeitbezug einer Kennzahl: siehe docs/KPI-KATALOG.md. */
|
||||
export type Bezug = "zeitraum" | "stichtag" | "gesamt";
|
||||
|
||||
const BEZUG_LABEL: Record<Bezug, string> = {
|
||||
@@ -19,14 +19,14 @@ const BEZUG_LABEL: Record<Bezug, string> = {
|
||||
* KPI-Karte nach UI_UX_GUIDELINES.md, Abschnitt 4:
|
||||
* Label oben links · Icon oben rechts · Wert · Zusatz · Fußzeile.
|
||||
*
|
||||
* Alle Karten einer Reihe haben dieselbe Anatomie — auch die ohne Trend.
|
||||
* Alle Karten einer Reihe haben dieselbe Anatomie, auch die ohne Trend.
|
||||
* Wo kein Vorperiodenvergleich möglich ist (stichtagsbezogene Kennzahlen),
|
||||
* trägt die Fußzeile den Zeitbezug. Ohne das steht in einer Reihe eine
|
||||
* Karte mit Trendzeile neben vier Karten mit leerem Fuß, und das Auge
|
||||
* sucht eine Regel, die es nicht gibt.
|
||||
*
|
||||
* Der Trend vergleicht immer mit der gleich langen Vorperiode
|
||||
* (30 Tage gegen die 30 Tage davor) — nicht mit dem Vorjahr.
|
||||
* (30 Tage gegen die 30 Tage davor): nicht mit dem Vorjahr.
|
||||
*/
|
||||
export function KPICard({
|
||||
label,
|
||||
@@ -38,7 +38,7 @@ export function KPICard({
|
||||
/**
|
||||
* Überschreibt die Fußzeile. Analytics hat einen seitenweiten Filter, deshalb
|
||||
* heißt es dort "im gewählten Zeitraum". Auf dem Dashboard gibt es den nicht
|
||||
* — jede Kachel trägt ihren eigenen Zeitraum und muss ihn benennen können.
|
||||
* jede Kachel trägt ihren eigenen Zeitraum und muss ihn benennen können.
|
||||
*/
|
||||
bezugText,
|
||||
icon,
|
||||
@@ -58,7 +58,7 @@ export function KPICard({
|
||||
verlauf?: number[];
|
||||
/**
|
||||
* Färbt das Icon. Nur setzen, wenn die Farbe aus einem Schwellenvergleich
|
||||
* kommt — ein dekorativ grünes Icon behauptet "gut", ohne es zu belegen
|
||||
* kommt, ein dekorativ grünes Icon behauptet "gut", ohne es zu belegen
|
||||
* (docs/DESIGN-SYSTEM.md §1: Statusfarben nur für Aussagen).
|
||||
*/
|
||||
akzent?: "neutral" | "positiv" | "warnung" | "negativ" | "info";
|
||||
@@ -85,14 +85,14 @@ export function KPICard({
|
||||
return (
|
||||
/* h-full: Die Karte füllt ihre Rasterzelle. Ohne das ist die Karte mit
|
||||
Trendzeile und Verlaufskurve höher als ihre Nachbarn, und eine Reihe
|
||||
gleichartiger Kennzahlen bekommt eine ungleiche Unterkante — auf dem
|
||||
gleichartiger Kennzahlen bekommt eine ungleiche Unterkante: auf dem
|
||||
Dashboard sichtbar, weil dort ein Link zwischen Zelle und Karte steht
|
||||
und die Streckung des Rasters nicht mehr durchreicht. */
|
||||
<article className="flex h-full min-h-[136px] flex-col rounded-lg border border-solid border-border-default bg-white p-4 shadow-sm transition-shadow duration-200 hover:shadow-md">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
{/* min-w-0 + break-words: Ohne das schiebt ein langes Label wie
|
||||
„Umsatz (Zahlungseingang)" das Icon aus der Karte und laeuft selbst
|
||||
ueber den Rand. Die Karte wird dadurch eine Zeile hoeher — das ist
|
||||
ueber den Rand. Die Karte wird dadurch eine Zeile hoeher, das ist
|
||||
der Preis dafuer, dass die Beschriftung vollstaendig lesbar bleibt
|
||||
(docs/DESIGN-SYSTEM.md §9). */}
|
||||
<span className="min-w-0 break-words text-[12px] font-medium leading-tight text-text-neutral-tertiary">
|
||||
@@ -150,7 +150,7 @@ export function KPICard({
|
||||
{verlauf && verlauf.length > 1 ? (
|
||||
<span className="h-8 w-16 flex-none">
|
||||
{/* Die Kurve trägt dieselbe Aussage wie der Pfeil daneben und
|
||||
deshalb auch dieselbe Farbe — sonst zeigt die Karte zwei
|
||||
deshalb auch dieselbe Farbe, sonst zeigt die Karte zwei
|
||||
widersprüchliche Signale. */}
|
||||
<Sparkline werte={verlauf} farbe={trend ? trendFarbe : CHART.achse} />
|
||||
</span>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React from "react";
|
||||
|
||||
/**
|
||||
* Kompakte Zusatzkennzahlen unter einem Chart —
|
||||
* Kompakte Zusatzkennzahlen unter einem Chart,
|
||||
* beantwortet "und was heißt das jetzt?" ohne eigene Karte.
|
||||
*/
|
||||
export function StatZeile({
|
||||
eintraege,
|
||||
/**
|
||||
* Spalten auf breiten Schirmen. In einer Drittelkarte brechen vier Spalten
|
||||
* mitten im Betrag um ("1,22 Mio." / "€") — dort gehoeren zwei hin.
|
||||
* mitten im Betrag um ("1,22 Mio." / "€"): dort gehoeren zwei hin.
|
||||
*/
|
||||
spalten = 4,
|
||||
}: {
|
||||
|
||||
@@ -31,7 +31,7 @@ import type { ZeitreihePunkt } from "../../data/types";
|
||||
*
|
||||
* Bewusst `ComposedChart`, nicht `AreaChart`: Recharts sammelt in einem
|
||||
* AreaChart ausschließlich `Area`-Kinder ein (generateCategoricalChart mit
|
||||
* GraphicalChild = Area). Ein `<Line>` darin wird stillschweigend verworfen —
|
||||
* GraphicalChild = Area). Ein `<Line>` darin wird stillschweigend verworfen,
|
||||
* die Vergleichsreihe stand monatelang in der Legende, ohne je gezeichnet zu
|
||||
* werden. ComposedChart nimmt beide Typen.
|
||||
*
|
||||
|
||||
@@ -9,14 +9,14 @@ import { ChartLeer } from "./ChartLeer";
|
||||
*
|
||||
* Warum kein Balken: Auslastungswerte liegen im Betrieb zwischen etwa 70 und
|
||||
* 100 %. Ein Balken muss bei 0 beginnen (sonst verzerrt die Länge), und dann
|
||||
* sind 74 % und 96 % optisch fast gleich lang — das Diagramm zeigt nichts.
|
||||
* sind 74 % und 96 % optisch fast gleich lang, das Diagramm zeigt nichts.
|
||||
* Ein Punkt kodiert über die Position, nicht über die Länge, und darf deshalb
|
||||
* einen ausgeschnittenen Bereich zeigen. Carbon nennt das Dot-Plot.
|
||||
*
|
||||
* Die Schwellen sind betrieblich gedacht, nicht mathematisch:
|
||||
* < 70 % Luft frei — Kapazität ungenutzt
|
||||
* < 70 % Luft frei Kapazität ungenutzt
|
||||
* 70–95 % Zielkorridor
|
||||
* > 95 % ausgebucht — Überlastrisiko, Termine gefährdet
|
||||
* > 95 % ausgebucht Überlastrisiko, Termine gefährdet
|
||||
* Der Korridor liegt als graues Band hinter den Punkten, die 100-%-Marke als
|
||||
* beschriftete Linie. Damit erklärt sich die Grafik ohne Ampel-Legende.
|
||||
*/
|
||||
@@ -109,10 +109,10 @@ export function AuslastungsBalken({
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Achse statt Farblegende — die Zonen erklären sich über das Band.
|
||||
{/* Achse statt Farblegende, die Zonen erklären sich über das Band.
|
||||
In sehr schmalen Karten bleibt für die Spur nur rund 40 px; dort
|
||||
stoßen selbst die beiden Randmarken aneinander. Eine Achse, die man
|
||||
nicht mehr lesen kann, ist kein Kompromiss, sondern Störung — sie
|
||||
nicht mehr lesen kann, ist kein Kompromiss, sondern Störung, sie
|
||||
entfällt dann, und die Erläuterung darunter trägt die Skala. */}
|
||||
<div className="hidden grid-cols-[minmax(96px,24%)_minmax(0,1fr)_48px] items-center gap-3 pt-1 @sm:grid">
|
||||
<span />
|
||||
@@ -120,7 +120,7 @@ export function AuslastungsBalken({
|
||||
{/* Beschriftungsdichte staffelt sich mit der Containerbreite: ganz
|
||||
schmal nur die Enden, dann die 100-%-Marke, dann jede zweite, dann
|
||||
alle. Container-Query, weil dieselbe Karte mal ein Drittel und mal
|
||||
die volle Seitenbreite einnimmt — der Viewport weiß davon nichts. */}
|
||||
die volle Seitenbreite einnimmt, der Viewport weiß davon nichts. */}
|
||||
{ticks.map((t, index) => {
|
||||
const stufe =
|
||||
t === min || t === max
|
||||
@@ -131,7 +131,7 @@ export function AuslastungsBalken({
|
||||
? "hidden @lg:inline"
|
||||
: "hidden @md:inline";
|
||||
// Zentriert man auch die Randmarken, ragt die Haelfte ihrer
|
||||
// Breite ueber die Spur hinaus — links wie rechts je rund 10 px.
|
||||
// Breite ueber die Spur hinaus: links wie rechts je rund 10 px.
|
||||
const rand =
|
||||
t === min ? "translate-x-0" : t === max ? "-translate-x-full" : "-translate-x-1/2";
|
||||
return (
|
||||
|
||||
@@ -24,7 +24,7 @@ export function BalkenHorizontal({
|
||||
einfarbig = true,
|
||||
farbeStart = 1,
|
||||
/**
|
||||
* Zweitwert rechts neben dem Hauptwert — z. B. Auftragswert neben der
|
||||
* Zweitwert rechts neben dem Hauptwert: z. B. Auftragswert neben der
|
||||
* Anzahl Abschlüsse. Erspart eine zweite Karte mit derselben Kodierung
|
||||
* auf derselben Personenachse.
|
||||
*/
|
||||
|
||||
@@ -52,7 +52,7 @@ export function BalkenVertikal({
|
||||
// Monatsnamen standen in einer Drittelkarte ohne Lücke aneinander.
|
||||
const intervall = achsenIntervall(punkte.length);
|
||||
|
||||
// Ganzzahlreihen brauchen ganzzahlige Ticks — sonst steht "1,5 Vorgänge".
|
||||
// Ganzzahlreihen brauchen ganzzahlige Ticks, sonst steht "1,5 Vorgänge".
|
||||
const maximum = Math.max(...punkte.map((p) => p.value));
|
||||
const raster = nurGanzzahlen
|
||||
? undefined
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { FeatherBarChart2 } from "@subframe/core";
|
||||
|
||||
/** Leerzustand — wichtiger als er aussieht: Filter können jedes Chart leeren. */
|
||||
/** Leerzustand: wichtiger als er aussieht: Filter können jedes Chart leeren. */
|
||||
export function ChartLeer({
|
||||
text = "Keine Daten im gewählten Zeitraum",
|
||||
hoehe = 220,
|
||||
|
||||
@@ -9,7 +9,7 @@ import React from "react";
|
||||
*/
|
||||
export function ChartLegende({
|
||||
eintraege,
|
||||
/** Ausgeblendete Reihen erscheinen ausgegraut — Klick schaltet um. */
|
||||
/** Ausgeblendete Reihen erscheinen ausgegraut: Klick schaltet um. */
|
||||
inaktiv = [],
|
||||
onToggle,
|
||||
}: {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { CHART, TOOLTIP_SCHATTEN } from "../../utils/chartTheme";
|
||||
*
|
||||
* Carbon-Vorgabe (@carbon/charts, .cds--cc--tooltip): helle Fläche
|
||||
* (--cds-layer-02), Text in --cds-text-primary, 12 px, abgesetzt allein durch
|
||||
* den Schatten — kein Rahmen, kein Doppelpunkt zwischen Name und Wert,
|
||||
* den Schatten, kein Rahmen, kein Doppelpunkt zwischen Name und Wert,
|
||||
* maximal 270 px breit.
|
||||
*
|
||||
* Bis Carbon v10 war der Tooltip dunkel (#393939). Viele Quellen im Netz
|
||||
|
||||
@@ -13,11 +13,11 @@ import { DONUT, farbeFuer, gruppiereRest } from "../../utils/chartTheme";
|
||||
* - maximal 5 Segmente, Rest als "Sonstige" (gruppiereRest)
|
||||
* - größtes Segment startet oben, dann im Uhrzeigersinn absteigend
|
||||
* - Gesamtwert steht in der Mitte, sonst muss man die Segmente addieren
|
||||
* - Legende rechts mit Wert — Carbon: „positioned to the right of the chart
|
||||
* - Legende rechts mit Wert: Carbon: „positioned to the right of the chart
|
||||
* when space is plentiful". Ist der Platz nicht reichlich, rutscht sie unter
|
||||
* den Ring, statt Namen und Werte ineinanderlaufen zu lassen. Gesteuert über
|
||||
* eine Container-Query, weil dieselbe Karte mal ein Drittel und mal die halbe
|
||||
* Seitenbreite einnimmt — der Viewport weiß davon nichts.
|
||||
* Seitenbreite einnimmt, der Viewport weiß davon nichts.
|
||||
* - Klick auf einen Legendeneintrag hebt das Segment hervor
|
||||
*/
|
||||
export function Donut({
|
||||
@@ -29,7 +29,7 @@ export function Donut({
|
||||
/**
|
||||
* Format der Ringmitte. Getrennt vom Legendenformat, weil die Mitte eine
|
||||
* Schlagzahl ist: Steht dieselbe Summe als Kennzahl auf der Seite, muss sie
|
||||
* gleich geschrieben sein — "285,3 T€" neben "285.318 €" liest sich wie
|
||||
* gleich geschrieben sein: "285,3 T€" neben "285.318 €" liest sich wie
|
||||
* zwei verschiedene Zahlen.
|
||||
*/
|
||||
formatMitte,
|
||||
|
||||
@@ -12,28 +12,28 @@ import { ChartLeer } from "./ChartLeer";
|
||||
* Übergangsquote ist die Zahl, nach der Betriebe ihren Vertrieb steuern:
|
||||
* Sie zeigt, wo Vorgänge verloren gehen.
|
||||
*
|
||||
* Einfarbig: Die Stufen sind eine Reihenfolge, keine unabhängigen Kategorien —
|
||||
* Einfarbig: Die Stufen sind eine Reihenfolge, keine unabhängigen Kategorien,
|
||||
* DESIGN-SYSTEM.md §1 „Rangfolgen einfarbig". Eine Farbabstufung würde nur
|
||||
* kodieren, was die Reihenfolge ohnehin sagt.
|
||||
*
|
||||
* Die Balken sind zentriert, nicht linksbündig: So liest man die Verjüngung als
|
||||
* Trichterform, ohne die gemeinsame Skala aufzugeben. Ein echtes Funnel-Chart
|
||||
* mit breitenkodierten Segmenten kennt Carbon bewusst nicht — es verzerrt.
|
||||
* mit breitenkodierten Segmenten kennt Carbon bewusst nicht, es verzerrt.
|
||||
*
|
||||
* **Zum Verhalten beim Überfahren:** Die Zeile unter dem Zeiger bekommt einen
|
||||
* Untergrund, die übrigen bleiben unverändert. Das war vorher andersherum
|
||||
* gelöst — die nicht berührten Balken wurden auf 45 % Deckkraft gesetzt — und
|
||||
* gelöst, die nicht berührten Balken wurden auf 45 % Deckkraft gesetzt, und
|
||||
* das war aus drei Gründen falsch:
|
||||
*
|
||||
* 1. Es hat mehr bewegt als gemeint war. Ein Zeiger, der über eine Zeile
|
||||
* streift, ließ fünf von sechs Balken verblassen; das liest sich wie eine
|
||||
* Auswahl, nicht wie ein Überfahren.
|
||||
* 2. Es war in sich widersprüchlich. Die Balken verblassten, die Namen und
|
||||
* Zahlen daneben blieben schwarz — dieselbe Zeile sagte gleichzeitig
|
||||
* Zahlen daneben blieben schwarz: dieselbe Zeile sagte gleichzeitig
|
||||
* „unwichtig" und „unverändert".
|
||||
* 3. Deckkraft trägt in diesem Diagramm bereits eine Bedeutung: Der Balken
|
||||
* kodiert einen Anteil. Ihn heller zu machen, heißt in der Sprache des
|
||||
* Diagramms „weniger" — und nicht „gerade nicht berührt".
|
||||
* Diagramms „weniger": und nicht „gerade nicht berührt".
|
||||
*
|
||||
* Der Untergrund ist derselbe wie in der Handlungsbedarf-Liste und in den
|
||||
* Tabellen (`bg-neutral-50`): Überfahren sieht überall gleich aus.
|
||||
|
||||
@@ -5,16 +5,16 @@ import { FeatherAlertTriangle, FeatherCheckCircle } from "@subframe/core";
|
||||
import { STATUS_FARBEN } from "../../utils/chartTheme";
|
||||
|
||||
/**
|
||||
* Der Begrüßungskopf des Dashboards — in der Software das `hero`-Widget
|
||||
* Der Begrüßungskopf des Dashboards, in der Software das `hero`-Widget
|
||||
* (`DashboardWidgetKind = "hero"`, siehe `dashboard-page.vm.ts`).
|
||||
*
|
||||
* Er beantwortet die einzige Frage, die vor allen Zahlen kommt: „Muss ich
|
||||
* heute etwas tun?" Deshalb steht neben der Anrede ein Satz zur Lage und
|
||||
* nicht etwa eine weitere Kennzahl — die kommen darunter.
|
||||
* nicht etwa eine weitere Kennzahl, die kommen darunter.
|
||||
*
|
||||
* Bewusst ohne Rahmen und ohne Karte: Eine Karte behauptet, hier stünden
|
||||
* Daten, die man mit den Nachbarkarten vergleichen kann. Das ist nicht der
|
||||
* Fall — das ist eine Ansprache. Sie trägt die Seitenüberschrift, deshalb
|
||||
* Fall, das ist eine Ansprache. Sie trägt die Seitenüberschrift, deshalb
|
||||
* hat diese Seite keinen `PageHeader` mit „Dashboard" darüber: Zwei
|
||||
* Überschriften übereinander, von denen eine den Namen der Seite und die
|
||||
* andere den Namen des Lesers nennt, sind eine zu viel.
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { ReservierterBereich } from "../../view-models/dashboard.vm";
|
||||
* Ein Platz, der noch kein Widget ist.
|
||||
*
|
||||
* Das Dashboard zeigt heute nur Analytics, wird aber mehr: eigene Aufgaben,
|
||||
* Termine des Tages, offene HR-Anträge — alle drei stehen im Designer-Brief
|
||||
* Termine des Tages, offene HR-Anträge, alle drei stehen im Designer-Brief
|
||||
* P11 und haben in der Software bereits ihre Widget-Domäne (`tasks`,
|
||||
* `schedule`, `hr`).
|
||||
*
|
||||
|
||||
@@ -9,8 +9,8 @@ import { ZEITRAUM_LABEL, type WidgetDomaene, type WidgetZeitraum } from "../../v
|
||||
/**
|
||||
* Rahmen einer Dashboard-Kachel.
|
||||
*
|
||||
* Sieht aus wie `ChartCard` aus Analytics — gleiche Kante, gleiche Abstände,
|
||||
* gleiche Schriftgrößen — trägt aber nicht deren Logik. Das ⋯-Menü und das
|
||||
* Sieht aus wie `ChartCard` aus Analytics: gleiche Kante, gleiche Abstände,
|
||||
* gleiche Schriftgrößen: trägt aber nicht deren Logik. Das ⋯-Menü und das
|
||||
* Ausblenden gehören zu Analytics; im Dashboard macht das die Widget-Registry
|
||||
* der Software (`docs/SCOPE-ANALYTICS-DASHBOARD.md`, Abschnitt 3.3).
|
||||
*
|
||||
@@ -23,7 +23,7 @@ import { ZEITRAUM_LABEL, type WidgetDomaene, type WidgetZeitraum } from "../../v
|
||||
*
|
||||
* 2. **Einen eigenen Zeitraum.** In Analytics gilt ein Filter für alle Karten.
|
||||
* Im Dashboard trägt jede Kachel ihren eigenen (P11: „Edit = nur
|
||||
* Widget-Filter/Zeitraum"). Deshalb steht er auf der Kachel — ohne das
|
||||
* Widget-Filter/Zeitraum"). Deshalb steht er auf der Kachel, ohne das
|
||||
* zeigt eine Kachel stillschweigend einen anderen Zeitraum als ihre
|
||||
* Nachbarin, und niemand merkt es.
|
||||
*/
|
||||
@@ -54,11 +54,11 @@ export function WidgetKachel({
|
||||
}: {
|
||||
titel: string;
|
||||
untertitel?: string;
|
||||
/** Erklärt die Kennzahl; erscheint als Tooltip am (i) — wie in Analytics. */
|
||||
/** Erklärt die Kennzahl; erscheint als Tooltip am (i): wie in Analytics. */
|
||||
hinweis?: string;
|
||||
domaene: WidgetDomaene;
|
||||
zeitraum: WidgetZeitraum;
|
||||
/** Ziel des Absprungs in die Analyse — `widget.drilldownHref` in der Software. */
|
||||
/** Ziel des Absprungs in die Analyse: `widget.drilldownHref` in der Software. */
|
||||
absprung?: string;
|
||||
absprungText?: string;
|
||||
children: React.ReactNode;
|
||||
@@ -67,7 +67,7 @@ export function WidgetKachel({
|
||||
return (
|
||||
<section
|
||||
/* @container: Die Kachelbreite hängt am Raster, nicht am Fenster. Eine
|
||||
Vier-Zwölftel-Kachel ist bei 1024 px rund 235 px breit — dieselbe
|
||||
Vier-Zwölftel-Kachel ist bei 1024 px rund 235 px breit: dieselbe
|
||||
Kachel bei 1920 px doppelt so viel. Was sich im Kopf einblendet, muss
|
||||
deshalb die Kachel messen und nicht den Viewport. */
|
||||
className={`@container flex h-full flex-col overflow-hidden rounded-lg border border-solid border-border-default bg-white shadow-sm ${className}`}
|
||||
@@ -112,7 +112,7 @@ export function WidgetKachel({
|
||||
{/* justify-center: Karten einer Zeile sind gleich hoch, aber nicht gleich
|
||||
voll. Eine Karte mit einem Balken neben einer mit einem Diagramm über
|
||||
400 px bekommt den Überschuss oben und unten je zur Hälfte, statt ihn
|
||||
unten abzuladen. Der Inhalt behält seine natürliche Höhe — die Karte
|
||||
unten abzuladen. Der Inhalt behält seine natürliche Höhe, die Karte
|
||||
wächst weiter mit, wenn mehr hineinkommt. */}
|
||||
<div className="flex min-h-0 grow flex-col justify-center px-4 pb-4">{children}</div>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from "react";
|
||||
/**
|
||||
* Das Kachelraster des Dashboards.
|
||||
*
|
||||
* Übernommen aus `DashboardPage.tsx` der Software — dort steht wörtlich
|
||||
* Übernommen aus `DashboardPage.tsx` der Software, dort steht wörtlich
|
||||
*
|
||||
* <div className="grid grid-cols-1 gap-4 md:grid-cols-2
|
||||
* lg:grid-flow-row-dense lg:grid-cols-12">
|
||||
@@ -15,7 +15,7 @@ import React from "react";
|
||||
* `{ size: { w: 8 }, position: { x: 0 } }`.
|
||||
*
|
||||
* Der Unterschied ist die Herkunft der Zahlen, nicht ihre Bedeutung. Im
|
||||
* Prototyp sind sie geschrieben, in der Software gespeichert — dort kann der
|
||||
* Prototyp sind sie geschrieben, in der Software gespeichert, dort kann der
|
||||
* Benutzer das Layout bearbeiten.
|
||||
*/
|
||||
export function WidgetRaster({ children }: { children: React.ReactNode }) {
|
||||
@@ -28,7 +28,7 @@ export function WidgetRaster({ children }: { children: React.ReactNode }) {
|
||||
|
||||
/**
|
||||
* Eine Rasterzelle. Zeichengleich zu `dashboardWidgetGridClass` in der
|
||||
* Software — bis auf einen Zusatz: Dort fehlt der Fall `x === 9`, weil noch
|
||||
* Software, bis auf einen Zusatz: Dort fehlt der Fall `x === 9`, weil noch
|
||||
* keine Rolle vier Kacheln nebeneinander stellt. Unsere Kennzahlenreihe tut
|
||||
* das, deshalb steht er hier. Beim Einbau ist das die eine Zeile, die in
|
||||
* `dashboardWidgetGridClass` zu ergänzen ist; ohne sie fließt die vierte
|
||||
@@ -41,15 +41,15 @@ export function Widget({
|
||||
mittelVoll = false,
|
||||
children,
|
||||
}: {
|
||||
/** Spalten von zwölf — entspricht `widget.size.w`. */
|
||||
/** Spalten von zwölf: entspricht `widget.size.w`. */
|
||||
breite: 3 | 4 | 6 | 8 | 12;
|
||||
/** Erste Spalte, nullbasiert — entspricht `widget.position.x`. */
|
||||
/** Erste Spalte, nullbasiert: entspricht `widget.position.x`. */
|
||||
start?: 0 | 3 | 4 | 6 | 8 | 9;
|
||||
/**
|
||||
* Volle Breite schon im Zweispalter zwischen `md` und `lg`, obwohl die
|
||||
* Kachel schmaler als sechs Zwölftel ist. Für die Fälle, in denen die
|
||||
* Nachbarkachel derselben Zeile breit ist und die schmale sonst allein in
|
||||
* einer halben Zeile stünde — die andere Hälfte bliebe leer.
|
||||
* einer halben Zeile stünde, die andere Hälfte bliebe leer.
|
||||
*/
|
||||
mittelVoll?: boolean;
|
||||
children: React.ReactNode;
|
||||
@@ -85,7 +85,7 @@ export function Widget({
|
||||
* werden darf, entscheidet der Inhalt:
|
||||
*
|
||||
* Kennzahlen und Listen (drei bis vier Zwölftel) vertragen eine halbe Spalte
|
||||
* — bei 768 px sind das rund 200 px, und mehr braucht eine Zahl mit Etikett
|
||||
* bei 768 px sind das rund 200 px, und mehr braucht eine Zahl mit Etikett
|
||||
* nicht. Diagramme (sechs Zwölftel und mehr) vertragen es nicht: Auf 200 px
|
||||
* rücken die Beschriftungen der Zeitachse ineinander, und die Balken der
|
||||
* Auslastung schrumpfen auf Strichbreite. Sie nehmen deshalb schon hier die
|
||||
|
||||
@@ -21,7 +21,7 @@ const ZEITRAEUME: ZeitraumPreset[] = ["heute", "7_tage", "30_tage", "quartal", "
|
||||
* Filterleiste über dem Seiteninhalt.
|
||||
*
|
||||
* Zeigt nur die Filter, die auf der jeweiligen Seite etwas bewirken
|
||||
* (FILTER_RELEVANZ) — ein Auftragsstatus-Filter auf der Finanzseite würde
|
||||
* (FILTER_RELEVANZ): ein Auftragsstatus-Filter auf der Finanzseite würde
|
||||
* sonst wirkungslos wirken und Vertrauen kosten.
|
||||
* Aktive Einschränkungen erscheinen darunter als entfernbare Chips (FILT-06).
|
||||
*/
|
||||
@@ -30,7 +30,7 @@ export function AnalyticsToolbar({
|
||||
optionen,
|
||||
}: {
|
||||
seite: SeitenId;
|
||||
/** Auswahllisten aus dem vollständigen Datensatz — in der Software aus dem Loader. */
|
||||
/** Auswahllisten aus dem vollständigen Datensatz, in der Software aus dem Loader. */
|
||||
optionen: FilterOptionen;
|
||||
}) {
|
||||
const { filter, setFilter, toggleWert, leereFeld, zuruecksetzen } = useFilter();
|
||||
@@ -70,7 +70,7 @@ export function AnalyticsToolbar({
|
||||
<div className="dv-no-print sticky top-14 z-20 flex flex-col gap-2 border-b border-solid border-border-default bg-neutral-50/95 px-4 py-3 backdrop-blur sm:px-6">
|
||||
{/* `relative` ist der Bezugsrahmen für die Filter-Panels: Unter `sm`
|
||||
hängen sie an dieser Zeile statt am einzelnen Knopf und nutzen die
|
||||
volle Breite — siehe MehrfachAuswahl. */}
|
||||
volle Breite: siehe MehrfachAuswahl. */}
|
||||
<div className="relative flex flex-wrap items-center gap-2">
|
||||
{/* Zeitraum als Segment-Control: eine Auswahl, immer sichtbar.
|
||||
*
|
||||
@@ -79,7 +79,7 @@ export function AnalyticsToolbar({
|
||||
* Ein Segment-Control ist ein Bauteil mit einem Rahmen, und die
|
||||
* Stufen stehen in einer Reihenfolge (heute → alles). Über zwei
|
||||
* Zeilen gebrochen liest es sich als zwei Steuerelemente, und die
|
||||
* Reihenfolge geht verloren. Deshalb scrollt es waagerecht — ohne
|
||||
* Reihenfolge geht verloren. Deshalb scrollt es waagerecht, ohne
|
||||
* sichtbare Leiste, weil der angeschnittene letzte Knopf schon
|
||||
* anzeigt, dass es weitergeht.
|
||||
*
|
||||
|
||||
@@ -53,7 +53,7 @@ export function MehrfachAuswahl({
|
||||
|
||||
return (
|
||||
// Unter `sm` bewusst *nicht* `relative`: Das Panel ist 256 px breit, der
|
||||
// vierte Filterknopf steht bei 375 px aber schon bei x = 278 — an den Knopf
|
||||
// vierte Filterknopf steht bei 375 px aber schon bei x = 278: an den Knopf
|
||||
// gehängt liefe es weit aus dem Bild. Ohne eigenen Bezugsrahmen hängt es
|
||||
// stattdessen an der Filterleiste (AnalyticsToolbar, `relative` an der
|
||||
// Knopfzeile) und nimmt dort die volle Zeilenbreite ein.
|
||||
|
||||
@@ -11,20 +11,20 @@ import type { SeitenId } from "../../data/filtering";
|
||||
/**
|
||||
* Innere Navigation des Analytics-Bereichs.
|
||||
*
|
||||
* Nachbau von `SidebarIconsLayout` aus dem Zielsystem — der Leiste, die dort
|
||||
* Nachbau von `SidebarIconsLayout` aus dem Zielsystem, der Leiste, die dort
|
||||
* jeder Arbeitsbereich mit Geschwisterseiten benutzt (Leads, Termine, HR,
|
||||
* Einstellungen; 17 Aufrufstellen). Analytics mit fünf Analyse-Seiten ist
|
||||
* derselbe Fall.
|
||||
*
|
||||
* Verhalten wie dort: eine schmale Icon-Leiste, die bei Zeigerkontakt oder
|
||||
* Tastaturfokus auf Beschriftungen aufklappt — verzögert, damit sie beim
|
||||
* Tastaturfokus auf Beschriftungen aufklappt: verzögert, damit sie beim
|
||||
* bloßen Überstreichen nicht aufspringt. Unterhalb `sm` tritt an ihre Stelle
|
||||
* eine Reiterleiste; auch das ist aus dem Original übernommen.
|
||||
*
|
||||
* ────────────────────────────────────────────────────────────────────────────
|
||||
* BEIM EINBAU: Dieser Nachbau wird NICHT portiert.
|
||||
*
|
||||
* Verbindlich ist die Verabredung — Analytics bekommt eine innere Leiste mit
|
||||
* Verbindlich ist die Verabredung: Analytics bekommt eine innere Leiste mit
|
||||
* den fünf Analyse-Seiten. Gebaut wird sie in HiveGrid mit dem dortigen
|
||||
* `SidebarIconsLayout`, das genau diesen Zweck hat (Leads, Termine, HR,
|
||||
* Einstellungen nutzen es bereits). Diese Datei beschreibt das Ziel, sie ist
|
||||
@@ -101,11 +101,11 @@ export function AnalyticsInnerNav({
|
||||
</SidebarInside>
|
||||
</div>
|
||||
|
||||
{/* Schmale Fenster: Reiter statt Leiste — wie im Original.
|
||||
{/* Schmale Fenster: Reiter statt Leiste, wie im Original.
|
||||
Waagerecht scrollend statt umbrechend: Fünf Reiter brauchen mindestens
|
||||
425 px, bei 375 px lief die ganze Seite dadurch seitlich über. Über
|
||||
zwei Zeilen gebrochen liest sich ein Reiterband als zwei Steuerelemente
|
||||
— dasselbe Mittel wie beim Zeitraum-Segment in der Filterleiste. */}
|
||||
dasselbe Mittel wie beim Zeitraum-Segment in der Filterleiste. */}
|
||||
<div className="dv-scroll-x w-full min-w-0 overflow-x-auto border-b border-solid border-border-default bg-white px-4 py-2 sm:hidden">
|
||||
<Tabs className="w-max min-w-full">
|
||||
{ANALYSE_SEITEN.map((seite) => (
|
||||
|
||||
@@ -13,12 +13,12 @@ import type { FilterOptionen } from "../../view-models/filter-optionen";
|
||||
*
|
||||
* „Analytics" ist in der Seitenleiste **ein** Menüpunkt. Die fünf Analyse-Seiten
|
||||
* sind darin über Reiter erreichbar und tauchen nicht als Unterpunkte in der
|
||||
* Navigation auf — sonst konkurrieren zwei Navigationsebenen um dieselbe Sache.
|
||||
* Navigation auf, sonst konkurrieren zwei Navigationsebenen um dieselbe Sache.
|
||||
*
|
||||
* Die Unter-Navigation ist eine **innere Leiste**, kein Reiterband: Das
|
||||
* Zielsystem benutzt für Arbeitsbereiche mit Geschwisterseiten durchgängig
|
||||
* `SidebarIconsLayout` (Leads, Termine, HR, Einstellungen). Reiter sind dort
|
||||
* für Abschnitte *innerhalb einer Seite* reserviert — und sind zugleich der
|
||||
* für Abschnitte *innerhalb einer Seite* reserviert, und sind zugleich der
|
||||
* Rückfall der inneren Leiste auf schmalen Fenstern. Begründung ausführlich
|
||||
* in docs/NAVIGATION.md.
|
||||
*
|
||||
|
||||
@@ -13,7 +13,7 @@ import { formatDatum, formatZahl } from "../../utils/formatters";
|
||||
import { HEUTE } from "../../utils/dateUtils";
|
||||
|
||||
/**
|
||||
* Rahmen der Anwendung — Seitenleiste nach dem Vorbild von HiveGrid Cloud.
|
||||
* Rahmen der Anwendung: Seitenleiste nach dem Vorbild von HiveGrid Cloud.
|
||||
*
|
||||
* Bewusst dieselbe Komponente wie dort (`~/ui/components/SidebarWithSections`)
|
||||
* mit denselben Gruppen und derselben Reihenfolge. Wer den Prototyp neben der
|
||||
@@ -22,7 +22,7 @@ import { HEUTE } from "../../utils/dateUtils";
|
||||
*
|
||||
* Jeder Eintrag führt auf eine echte Seite. „Dashboard" landet auf einer
|
||||
* Platzhalterseite, weil es ein eigenes Feature ist und hier nicht gestaltet
|
||||
* wird — aber ein Menüpunkt, der nur einen Hinweis auslöst, lässt offen, ob
|
||||
* wird, aber ein Menüpunkt, der nur einen Hinweis auslöst, lässt offen, ob
|
||||
* etwas fehlt oder etwas kaputt ist.
|
||||
*
|
||||
* ────────────────────────────────────────────────────────────────────────────
|
||||
@@ -31,7 +31,7 @@ import { HEUTE } from "../../utils/dateUtils";
|
||||
* Sie existiert, damit der Prototyp alleine lauffähig ist. In HiveGrid Cloud
|
||||
* übernehmen `SubframeAppShell` und `AppSidebar`. Verbindlich ist aus dieser
|
||||
* Datei allein die **Struktur**: zwei Einträge in der Seitenleiste, Dashboard
|
||||
* und Analytics — mehr nicht, weil alles Übrige dort schon steht.
|
||||
* und Analytics, mehr nicht, weil alles Übrige dort schon steht.
|
||||
*
|
||||
* Globale Regeln (Schrift, Größen, Abstände, Farben, Control-Höhen) kommen
|
||||
* ausschließlich aus HiveGrid und werden von hier aus nicht überschrieben.
|
||||
@@ -48,7 +48,7 @@ export function AppShell({ children }: { children: React.ReactNode }) {
|
||||
const istSchmal = useIstSchmal();
|
||||
const ausloeser = React.useRef<HTMLButtonElement>(null);
|
||||
|
||||
// Beim Seitenwechsel schließen — sonst bleibt die Leiste über dem Ziel
|
||||
// Beim Seitenwechsel schließen, sonst bleibt die Leiste über dem Ziel
|
||||
// liegen. Der Pfad wandert dafür in den State: Zustand beim Rendern
|
||||
// anzupassen ist hier richtig, ein Effekt würde einen zweiten Durchlauf
|
||||
// auslösen und die Leiste kurz über der neuen Seite zeigen.
|
||||
@@ -69,7 +69,7 @@ export function AppShell({ children }: { children: React.ReactNode }) {
|
||||
* Nicht optional: Der Rahmen trägt `role="dialog"` und `aria-modal`, und
|
||||
* damit ist einem Screenreader zugesagt, dass nichts dahinter erreichbar ist.
|
||||
* Ohne Falle wandert der Tabulator hinter die Abdeckung in eine Seite, die
|
||||
* optisch verdeckt ist — die Zusage wäre eine Unwahrheit.
|
||||
* optisch verdeckt ist, die Zusage wäre eine Unwahrheit.
|
||||
*
|
||||
* Übernommen aus `SubframeAppShell` im Zielsystem: Tab und Umschalt+Tab
|
||||
* laufen zyklisch, Escape schließt und gibt den Fokus an den Auslöser
|
||||
@@ -78,7 +78,7 @@ export function AppShell({ children }: { children: React.ReactNode }) {
|
||||
* Beim Öffnen bekommt der **Rahmen** den Fokus, nicht der erste Link. Das
|
||||
* ist das übliche Vorgehen für einen Dialog und hat hier einen sichtbaren
|
||||
* Grund: Ein per Skript fokussierter Link zeigt in Chrome seinen Fokusring,
|
||||
* auch wenn die Schublade mit der Maus geöffnet wurde — dann liegt ein
|
||||
* auch wenn die Schublade mit der Maus geöffnet wurde, dann liegt ein
|
||||
* blauer Rahmen um „Dashboard", ohne dass jemand dorthin getippt hat. Der
|
||||
* Rahmen selbst trägt `tabIndex={-1}` und keinen Ring; die Sprachausgabe
|
||||
* liest trotzdem den Dialog an, und der erste Tabulatorschritt landet auf
|
||||
@@ -112,7 +112,7 @@ export function AppShell({ children }: { children: React.ReactNode }) {
|
||||
const aktiv = document.activeElement;
|
||||
// `aktiv === leiste` gehört dazu: Direkt nach dem Öffnen liegt der Fokus
|
||||
// auf dem Rahmen selbst. Er steht im Dokument vor seinen Kindern, also
|
||||
// muss Umschalt+Tab von dort ans Ende springen — sonst verlässt der
|
||||
// muss Umschalt+Tab von dort ans Ende springen, sonst verlässt der
|
||||
// Tabulator den Dialog rückwärts, und die Zusage von `aria-modal` wäre
|
||||
// gebrochen.
|
||||
if (event.shiftKey && (aktiv === erstes || aktiv === leiste || !leiste.contains(aktiv))) {
|
||||
@@ -132,13 +132,13 @@ export function AppShell({ children }: { children: React.ReactNode }) {
|
||||
/**
|
||||
* Zuletzt benutzte Eingabeart am Wurzelelement vermerken.
|
||||
*
|
||||
* `:focus-visible` allein reicht nicht: Wenn ein Skript den Fokus setzt —
|
||||
* beim Schließen der Schublade zurück auf den Hamburger — behandelt Chrome
|
||||
* `:focus-visible` allein reicht nicht: Wenn ein Skript den Fokus setzt,
|
||||
* beim Schließen der Schublade zurück auf den Hamburger: behandelt Chrome
|
||||
* das wie Tastaturbedienung und zeigt den Ring. Nach einem Mausklick steht
|
||||
* er dann sichtbar da, bis man woanders hinklickt.
|
||||
*
|
||||
* Die Regel dazu steht in `app/globals.css`. Sie unterdrückt den Ring nur,
|
||||
* solange die letzte Eingabe ein Zeiger war, und nur an Knöpfen und Links —
|
||||
* solange die letzte Eingabe ein Zeiger war, und nur an Knöpfen und Links,
|
||||
* Eingabefelder behalten ihn immer, dort gehört er zum Zustand „hier wird
|
||||
* gerade geschrieben". Der erste Tastendruck schaltet ihn sofort zurück.
|
||||
*/
|
||||
@@ -203,8 +203,8 @@ export function AppShell({ children }: { children: React.ReactNode }) {
|
||||
|
||||
{/* z-50 ist nötig, nicht dekorativ: `position: sticky` erzeugt einen
|
||||
eigenen Stapelkontext. Ohne z-Wert am Rahmen bleibt das z-index des
|
||||
Einklapp-Knopfes darin gefangen, und die Inhaltsspalte — ein späteres
|
||||
Geschwisterelement — malt über den Teil, der aus der Leiste
|
||||
Einklapp-Knopfes darin gefangen, und die Inhaltsspalte, ein späteres
|
||||
Geschwisterelement: malt über den Teil, der aus der Leiste
|
||||
herausragt. Die Software setzt an derselben Stelle ebenfalls z-50. */}
|
||||
<div
|
||||
ref={leisteRef}
|
||||
|
||||
@@ -2,11 +2,11 @@ import React from "react";
|
||||
|
||||
/**
|
||||
* Inhaltsspalte einer Seite.
|
||||
* 24 px Außenabstand, 16 px Rasterabstand — entspricht dem 8-px-System
|
||||
* 24 px Außenabstand, 16 px Rasterabstand: entspricht dem 8-px-System
|
||||
* aus den UI-Guidelines (internal ≤ external).
|
||||
*
|
||||
* Unter `sm` sind 24 px Rand ein Achtel der Fensterbreite: Der Rand kostet
|
||||
* dort mehr Platz, als er Ruhe bringt. Deshalb 16 px — der nächste Wert der
|
||||
* dort mehr Platz, als er Ruhe bringt. Deshalb 16 px, der nächste Wert der
|
||||
* Skala, nicht ein neuer.
|
||||
*/
|
||||
export function PageContainer({ children }: { children: React.ReactNode }) {
|
||||
@@ -32,7 +32,7 @@ export function Raster({
|
||||
}) {
|
||||
/**
|
||||
* Jede Leiter beginnt einspaltig. Zwei Spalten sind bei 375 px rund 155 px
|
||||
* je Karte — darin bricht ein Währungswert zwischen Betrag und Euro-Zeichen
|
||||
* je Karte: darin bricht ein Währungswert zwischen Betrag und Euro-Zeichen
|
||||
* um, und die Karte wird höher statt schmaler.
|
||||
*
|
||||
* Wann die erste Verdopplung kommt, hängt am Inhalt, nicht an der Zielzahl:
|
||||
@@ -42,7 +42,7 @@ export function Raster({
|
||||
* Diagramme und Tabellen, die halbiert erst ab `md` lesbar bleiben.
|
||||
*
|
||||
* Keine Leiter erhöht bei `md`: Dort kommt die Seitenleiste zurück und
|
||||
* nimmt 240 px weg — die Karten würden schmaler statt breiter.
|
||||
* nimmt 240 px weg, die Karten würden schmaler statt breiter.
|
||||
*
|
||||
* Ab 1024 px ist jede Leiter identisch zum abgenommenen Stand.
|
||||
*/
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useToast } from "../../context/ToastContext";
|
||||
|
||||
/**
|
||||
* Kopf einer Analyse-Seite: Titel, Einordnung, Seitenaktionen.
|
||||
* Die Aktionen sind im Prototyp bewusst folgenlos (Toast statt Export) —
|
||||
* Die Aktionen sind im Prototyp bewusst folgenlos (Toast statt Export),
|
||||
* siehe HANDOVER.md, Abschnitt "Was noch Logik braucht".
|
||||
*/
|
||||
export function PageHeader({
|
||||
@@ -15,7 +15,7 @@ export function PageHeader({
|
||||
aktionen,
|
||||
/**
|
||||
* Aktualisieren, Export und Teilen. Auf Seiten ohne Inhalt gibt es nichts zu
|
||||
* aktualisieren oder zu exportieren — dort schaltet man sie ab, statt
|
||||
* aktualisieren oder zu exportieren, dort schaltet man sie ab, statt
|
||||
* Schaltflächen anzubieten, die ins Leere greifen.
|
||||
*/
|
||||
standardAktionen = true,
|
||||
@@ -29,8 +29,8 @@ export function PageHeader({
|
||||
|
||||
/**
|
||||
* Unter `sm` tragen die beiden beschrifteten Knöpfe nur ihr Icon.
|
||||
* Nebeneinander brauchen "Aktualisieren" und "Export" rund 240 px — mehr,
|
||||
* als bei 375 px neben dem Titel überhaupt vorhanden ist. Die Beschriftung
|
||||
* Nebeneinander brauchen "Aktualisieren" und "Export" rund 240 px. Das ist
|
||||
* mehr, als bei 375 px neben dem Titel überhaupt vorhanden ist. Die Beschriftung
|
||||
* entfällt, `aria-label` bleibt: Für die Sprachausgabe ändert sich nichts.
|
||||
*/
|
||||
const knopf =
|
||||
@@ -40,7 +40,7 @@ export function PageHeader({
|
||||
|
||||
return (
|
||||
// Unter `sm` untereinander: Titel und Aktionen nebeneinander lassen für den
|
||||
// Titel keine 130 px übrig — er zerfiele in vier Zeilen, während die Knöpfe
|
||||
// Titel keine 130 px übrig, er zerfiele in vier Zeilen, während die Knöpfe
|
||||
// aus dem Bild laufen.
|
||||
<div className="flex flex-col items-stretch gap-4 px-4 pb-6 pt-6 sm:flex-row sm:flex-wrap sm:items-start sm:justify-between sm:px-6">
|
||||
<div className="flex min-w-0 flex-col gap-0">
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { SeitenId } from "../../data/filtering";
|
||||
* Navigationsstruktur des Prototyps.
|
||||
*
|
||||
* Hier stehen **nur unsere beiden Features**: Dashboard und Analytics. Alles
|
||||
* andere — Kontakte, Leads, Pipelines, Postfach, Termine, Einstellungen — ist
|
||||
* andere (Kontakte, Leads, Pipelines, Postfach, Termine, Einstellungen) ist
|
||||
* in der Seitenleiste der Software bereits vorhanden und wird hier nicht
|
||||
* verdoppelt. Der Prototyp zeigt, was dazukommt, nicht was schon da ist.
|
||||
*
|
||||
@@ -37,7 +37,7 @@ export interface NavGruppe {
|
||||
/** Einstieg des Analytics-Bereichs. */
|
||||
export const ANALYTICS_START = "/analytics";
|
||||
|
||||
/** Der Begrüßungsbildschirm — Ziel der Weiterleitung von „/", wie in der Software. */
|
||||
/** Der Begrüßungsbildschirm: Ziel der Weiterleitung von „/", wie in der Software. */
|
||||
export const DASHBOARD_START = "/dashboard";
|
||||
|
||||
export const NAV_GRUPPEN: NavGruppe[] = [
|
||||
@@ -46,7 +46,7 @@ export const NAV_GRUPPEN: NavGruppe[] = [
|
||||
titel: "",
|
||||
eintraege: [
|
||||
{
|
||||
// Der Begrüßungsbildschirm — eine einzelne Seite, deshalb ohne innere
|
||||
// Der Begrüßungsbildschirm, eine einzelne Seite, deshalb ohne innere
|
||||
// Leiste. Er ist das Ziel der Weiterleitung von „/".
|
||||
id: "dashboard",
|
||||
label: "Dashboard",
|
||||
@@ -66,10 +66,10 @@ export const NAV_GRUPPEN: NavGruppe[] = [
|
||||
];
|
||||
|
||||
/**
|
||||
* Unterer Bereich, durch einen Trenner abgesetzt — wie in der Software.
|
||||
* Unterer Bereich, durch einen Trenner abgesetzt, wie in der Software.
|
||||
*
|
||||
* Hier steht nur, was zum Prototyp gehört: die Bausteingalerie. Sie ist für
|
||||
* die **Abstimmung** da, nicht für die Umsetzung — sie zeigt jeden Baustein in
|
||||
* die **Abstimmung** da, nicht für die Umsetzung, sie zeigt jeden Baustein in
|
||||
* allen Zuständen, auch in denen, die die Demo-Daten nie erzeugen (negativer
|
||||
* Trend, leer, Fehler, „kleiner ist besser"). Wer den Prototyp einbaut,
|
||||
* braucht sie nicht: Dafür sind die Datei-Pfade und `docs/DESIGN-SYSTEM.md`
|
||||
@@ -88,7 +88,7 @@ export const NAV_SEKUNDAER: NavEintrag[] = [
|
||||
|
||||
/**
|
||||
* Die fünf Analyse-Seiten. Sie erscheinen **nicht** in der Seitenleiste,
|
||||
* sondern als Reiter innerhalb des Menüpunkts „Analytics" — dem Hausmuster
|
||||
* sondern als Reiter innerhalb des Menüpunkts „Analytics": dem Hausmuster
|
||||
* für Unter-Navigation in HiveGrid (`SettingsHubShell.tsx`: ein `Link` um ein
|
||||
* `Tabs.Item`, damit jeder Reiter eine echte Navigation ist).
|
||||
*/
|
||||
@@ -107,21 +107,21 @@ export const ANALYSE_SEITEN: AnalyseSeite[] = [
|
||||
icon: "LayoutDashboard",
|
||||
label: "Übersicht",
|
||||
href: "/analytics",
|
||||
beschreibung: "Die Kennzahlen, nach denen der Betrieb gesteuert wird — Umsatz, Auslastung, offene Posten und Handlungsbedarf.",
|
||||
beschreibung: "Die Kennzahlen, nach denen der Betrieb gesteuert wird: Umsatz, Auslastung, offene Posten und Handlungsbedarf.",
|
||||
},
|
||||
{
|
||||
id: "finanzen",
|
||||
icon: "Euro",
|
||||
label: "Finanzen",
|
||||
href: "/analytics/finanzen",
|
||||
beschreibung: "Umsatz, Zahlungseingang und offene Forderungen — die Sicht der Buchhaltung auf den Betrieb.",
|
||||
beschreibung: "Umsatz, Zahlungseingang und offene Forderungen: die Sicht der Buchhaltung auf den Betrieb.",
|
||||
},
|
||||
{
|
||||
id: "auftraege",
|
||||
icon: "ClipboardList",
|
||||
label: "Aufträge",
|
||||
href: "/analytics/auftraege",
|
||||
beschreibung: "Von der Anfrage bis zur Abnahme — wo die Vorgänge stehen, was gebunden ist und was über Termin läuft.",
|
||||
beschreibung: "Von der Anfrage bis zur Abnahme: wo die Vorgänge stehen, was gebunden ist und was über Termin läuft.",
|
||||
},
|
||||
{
|
||||
id: "kunden",
|
||||
@@ -135,6 +135,6 @@ export const ANALYSE_SEITEN: AnalyseSeite[] = [
|
||||
icon: "HardHat",
|
||||
label: "Team",
|
||||
href: "/analytics/team",
|
||||
beschreibung: "Auslastung, produktive Stunden und Abschlüsse der Monteure — die Kennzahl hinter der Terminplanung.",
|
||||
beschreibung: "Auslastung, produktive Stunden und Abschlüsse der Monteure: die Kennzahl hinter der Terminplanung.",
|
||||
},
|
||||
];
|
||||
@@ -14,7 +14,7 @@ export interface Spalte<T> {
|
||||
titel: string;
|
||||
/** Zellinhalt; darf beliebige Knoten liefern (Badges, Links). */
|
||||
zelle: (zeile: T) => React.ReactNode;
|
||||
/** Sortierwert — ohne diesen ist die Spalte nicht sortierbar. */
|
||||
/** Sortierwert, ohne diesen ist die Spalte nicht sortierbar. */
|
||||
sortWert?: (zeile: T) => number | string;
|
||||
/** Zahlen gehören rechtsbündig und in Tabellenziffern. */
|
||||
numerisch?: boolean;
|
||||
@@ -26,7 +26,7 @@ export interface Spalte<T> {
|
||||
*
|
||||
* Enthält die Interaktionen, die man in einer Demo wirklich braucht:
|
||||
* Sortieren per Klick auf den Spaltenkopf, Volltextsuche, Blättern.
|
||||
* Alles clientseitig — bei echten Datenmengen gehört das auf den Server.
|
||||
* Alles clientseitig, bei echten Datenmengen gehört das auf den Server.
|
||||
*/
|
||||
export function DataTable<T extends { id: string }>({
|
||||
zeilen,
|
||||
@@ -105,7 +105,7 @@ export function DataTable<T extends { id: string }>({
|
||||
{/* Der waagerechte Scrollbereich der Tabelle. Ohne min-w-0 am Elternteil
|
||||
greift er nicht: Ein Flex-Kind wird nicht schmaler als sein Inhalt,
|
||||
die Tabelle bliebe also breit und schöbe stattdessen die Seite auf.
|
||||
Vorher trug der Bereich zusätzlich -mx-1/px-1 — das machte ihn 8 px
|
||||
Vorher trug der Bereich zusätzlich -mx-1/px-1: das machte ihn 8 px
|
||||
breiter als seine Spalte und ließ die Karte um 4 px überlaufen. Den
|
||||
Fokusring der Sortierknöpfe deckt bereits das px-2 der Kopfzelle. */}
|
||||
<div className="dv-scroll w-full min-w-0 overflow-x-auto">
|
||||
|
||||
@@ -6,7 +6,7 @@ import { erstellePersistentenStore } from "./persistenterStore";
|
||||
|
||||
/**
|
||||
* Globaler Filter-State über alle Analytics-Seiten hinweg (INFRA-01),
|
||||
* persistiert in localStorage (INFRA-02) — ein Reload verwirft die Auswahl
|
||||
* persistiert in localStorage (INFRA-02): ein Reload verwirft die Auswahl
|
||||
* nicht, im Kundentermin ein spürbarer Unterschied.
|
||||
*/
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { FeatherCheck, FeatherInfo } from "@subframe/core";
|
||||
/**
|
||||
* Minimale Rückmeldung für Demo-Aktionen (Export, Speichern, Teilen).
|
||||
*
|
||||
* Im Prototyp passiert dahinter nichts — der Toast macht aber sichtbar,
|
||||
* Im Prototyp passiert dahinter nichts, der Toast macht aber sichtbar,
|
||||
* dass der Klick registriert wurde, statt dass der Button "tot" wirkt.
|
||||
* In der echten Software durch die vorhandene Toast-Komponente ersetzen.
|
||||
*/
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* das eine zweite Renderrunde aus (und React beanstandet das zu Recht).
|
||||
* `useSyncExternalStore` ist genau für diesen Fall gebaut: Es rendert
|
||||
* serverseitig den Standardwert und wechselt nach der Hydration auf den
|
||||
* gespeicherten — ohne Mismatch und ohne Zusatzrender.
|
||||
* gespeicherten, ohne Mismatch und ohne Zusatzrender.
|
||||
*/
|
||||
|
||||
export interface PersistenterStore<T> {
|
||||
@@ -19,7 +19,7 @@ export interface PersistenterStore<T> {
|
||||
export function erstellePersistentenStore<T>(
|
||||
schluessel: string,
|
||||
standard: T,
|
||||
/** Gelesene Daten mit dem Standard verschmelzen — fängt alte Formate ab. */
|
||||
/** Gelesene Daten mit dem Standard verschmelzen: fängt alte Formate ab. */
|
||||
zusammenfuehren: (gespeichert: unknown, standard: T) => T = (g, s) =>
|
||||
({ ...s, ...(g as object) }) as T
|
||||
): PersistenterStore<T> {
|
||||
@@ -52,7 +52,7 @@ export function erstellePersistentenStore<T>(
|
||||
try {
|
||||
window.localStorage.setItem(schluessel, JSON.stringify(wert));
|
||||
} catch {
|
||||
// Privater Modus o. Ä. — Persistenz ist optional.
|
||||
// Privater Modus o. Ä.: Persistenz ist optional.
|
||||
}
|
||||
hoerer.forEach((h) => h());
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Kennzahlen-Berechnungen.
|
||||
*
|
||||
* Alle Funktionen sind pure functions über dem gefilterten Datensatz
|
||||
* (Anforderung INFRA-06) — die Entwicklung kann sie unverändert übernehmen
|
||||
* (Anforderung INFRA-06): die Entwicklung kann sie unverändert übernehmen
|
||||
* und lediglich die Datenquelle austauschen.
|
||||
*
|
||||
* Die Definition jeder Kennzahl steht in docs/KPI-KATALOG.md.
|
||||
@@ -46,7 +46,7 @@ export function umsatz(rechnungen: Rechnung[], zeitraum: ZeitraumPreset): number
|
||||
.reduce((summe, r) => summe + r.betrag, 0);
|
||||
}
|
||||
|
||||
/** Gleiche Berechnung für die Vorperiode — Basis der Trend-Badges. */
|
||||
/** Gleiche Berechnung für die Vorperiode: Basis der Trend-Badges. */
|
||||
export function umsatzVorperiode(rechnungen: Rechnung[], zeitraum: ZeitraumPreset): number {
|
||||
const fenster = vorperiode(zeitraum);
|
||||
return rechnungen
|
||||
@@ -54,7 +54,7 @@ export function umsatzVorperiode(rechnungen: Rechnung[], zeitraum: ZeitraumPrese
|
||||
.reduce((summe, r) => summe + r.betrag, 0);
|
||||
}
|
||||
|
||||
/** Beschriftung eines Buckets — eine Stelle fuer alle drei Zeitreihen. */
|
||||
/** Beschriftung eines Buckets, eine Stelle fuer alle drei Zeitreihen. */
|
||||
function bucketLabel(key: string, art: "tag" | "woche" | "monat"): string {
|
||||
if (art === "monat") return formatMonatKurz(key);
|
||||
if (art === "woche") return formatWoche(key);
|
||||
@@ -171,7 +171,7 @@ export function zahlungsdauer(rechnungen: Rechnung[], zeitraum: ZeitraumPreset):
|
||||
* Zusammensetzung des OFFENEN Bestands.
|
||||
*
|
||||
* Nicht zu verwechseln mit rechnungenNachStatus(): dort steht der kumulierte
|
||||
* Umsatz aus zwei Jahren gegen den heutigen Forderungsbestand — dabei entfallen
|
||||
* Umsatz aus zwei Jahren gegen den heutigen Forderungsbestand, dabei entfallen
|
||||
* über 90 % auf "Bezahlt", und der Ring zeigt nichts mehr. Der offene Bestand
|
||||
* ist ein echtes Ganzes: alles, was heute noch aussteht.
|
||||
*/
|
||||
@@ -205,13 +205,13 @@ export function auftraegeImZeitraum(auftraege: Auftrag[], zeitraum: ZeitraumPres
|
||||
return auftraege.filter((a) => imFenster(a.angelegtAm, fenster));
|
||||
}
|
||||
|
||||
/** "Aktiv" = beauftragt oder in Arbeit — das, was das Team gerade bindet. */
|
||||
/** "Aktiv" = beauftragt oder in Arbeit: das, was das Team gerade bindet. */
|
||||
export function aktiveAuftraege(auftraege: Auftrag[]): Auftrag[] {
|
||||
return auftraege.filter((a) => a.status === "beauftragt" || a.status === "in_arbeit");
|
||||
}
|
||||
|
||||
export function ueberfaelligeAuftraege(auftraege: Auftrag[]): Auftrag[] {
|
||||
// Einen Termin gibt es erst ab der Beauftragung — eine offene Anfrage kann
|
||||
// Einen Termin gibt es erst ab der Beauftragung, eine offene Anfrage kann
|
||||
// nicht "über Termin" sein.
|
||||
return auftraege
|
||||
.filter(
|
||||
@@ -264,7 +264,7 @@ export function umsatzNachGewerk(
|
||||
* Verlorene Aufträge zählen nur bis zu ihrer letzten erreichten Stufe.
|
||||
*/
|
||||
/**
|
||||
* Vertriebstrichter — bewusst nur über das Angebotsgeschäft.
|
||||
* Vertriebstrichter: bewusst nur über das Angebotsgeschäft.
|
||||
*
|
||||
* Wartung, Notdienst und E-Check laufen ohne Angebot direkt in die Ausführung.
|
||||
* Zählte man sie mit, würde der Trichter unten breiter als oben und die
|
||||
@@ -335,7 +335,7 @@ export function neueKunden(kunden: Kunde[], zeitraum: ZeitraumPreset): number {
|
||||
return kunden.filter((k) => imFenster(k.angelegtAm, fenster)).length;
|
||||
}
|
||||
|
||||
/** Gleiches Fenster davor — Basis fuer den Trend an der Neukunden-Karte. */
|
||||
/** Gleiches Fenster davor: Basis fuer den Trend an der Neukunden-Karte. */
|
||||
export function neueKundenVorperiode(kunden: Kunde[], zeitraum: ZeitraumPreset): number {
|
||||
const fenster = vorperiode(zeitraum);
|
||||
return kunden.filter((k) => imFenster(k.angelegtAm, fenster)).length;
|
||||
@@ -455,7 +455,7 @@ export interface AlertEintrag {
|
||||
text: string;
|
||||
detail: string;
|
||||
art: "kritisch" | "warnung" | "hinweis";
|
||||
/** Betrag, um den es geht — macht die Zeilen untereinander vergleichbar. */
|
||||
/** Betrag, um den es geht: macht die Zeilen untereinander vergleichbar. */
|
||||
betrag?: number;
|
||||
/**
|
||||
* Die Handlung, nicht die Beobachtung. Eine Liste ohne Verb ist ein Bericht;
|
||||
@@ -538,7 +538,7 @@ export function handlungsbedarf(daten: Datensatz): AlertEintrag[] {
|
||||
*
|
||||
* Bezugsgröße sind ausschließlich förderfähige Leistungen (Wärmepumpe, PV,
|
||||
* Speicher, Lüftung). Über alle Aufträge gerechnet läge die Quote im
|
||||
* einstelligen Bereich und wäre wertlos — eine Badwartung war nie förderfähig.
|
||||
* einstelligen Bereich und wäre wertlos, eine Badwartung war nie förderfähig.
|
||||
*/
|
||||
export function foerderQuote(auftraege: Auftrag[]): {
|
||||
quote: number;
|
||||
|
||||
@@ -26,7 +26,7 @@ export const FILTER_RELEVANZ: Record<SeitenId, FilterFeld[]> = {
|
||||
|
||||
/**
|
||||
* Setzt alle für die Seite irrelevanten Filter zurück, ohne sie im
|
||||
* globalen State zu löschen — wechselt man die Seite, sind sie wieder da.
|
||||
* globalen State zu löschen: wechselt man die Seite, sind sie wieder da.
|
||||
*/
|
||||
export function wirksameFilter(filter: FilterState, seite: SeitenId): FilterState {
|
||||
const erlaubt = new Set(FILTER_RELEVANZ[seite]);
|
||||
@@ -41,7 +41,7 @@ export function wirksameFilter(filter: FilterState, seite: SeitenId): FilterStat
|
||||
}
|
||||
|
||||
/**
|
||||
* Wendet die Dimensionsfilter an — bewusst OHNE Zeitfilter.
|
||||
* Wendet die Dimensionsfilter an: bewusst OHNE Zeitfilter.
|
||||
*
|
||||
* Grund: Kennzahlen brauchen unterschiedliche Zeitbezüge (Umsatz im Zeitraum,
|
||||
* offene Rechnungen dagegen stichtagsbezogen). Der Zeitraum wird deshalb
|
||||
@@ -77,7 +77,7 @@ export function wendeFilterAn(daten: Datensatz, filter: FilterState): Datensatz
|
||||
return { mitarbeiter, kunden, auftraege, rechnungen, zeiten };
|
||||
}
|
||||
|
||||
/** Anzahl aktiver Einschränkungen — für das Badge am Filter-Button. */
|
||||
/** Anzahl aktiver Einschränkungen, für das Badge am Filter-Button. */
|
||||
export function anzahlAktiverFilter(filter: FilterState, seite: SeitenId): number {
|
||||
const wirksam = wirksameFilter(filter, seite);
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Der angemeldete Benutzer — im Prototyp eine Attrappe.
|
||||
* Der angemeldete Benutzer, im Prototyp eine Attrappe.
|
||||
*
|
||||
* In HiveGrid Cloud kommt das aus der Sitzung (`requireMember`) und steht dem
|
||||
* Loader zur Verfügung, bevor die Seite rendert. Hier ist es eine Konstante,
|
||||
@@ -16,7 +16,7 @@ export const NUTZER = {
|
||||
* Für die Anrede. In eckigen Klammern und nicht als erfundener Name:
|
||||
* „Guten Tag, Dejan" sieht in einer Abnahme aus wie ein fest verdrahteter
|
||||
* Wert, und irgendwann fragt jemand, warum dort immer derselbe Name steht.
|
||||
* „[Vorname]" sagt stattdessen, was an dieser Stelle eingesetzt wird — beim
|
||||
* „[Vorname]" sagt stattdessen, was an dieser Stelle eingesetzt wird: beim
|
||||
* Einbau kommt er aus der Sitzung.
|
||||
*/
|
||||
vorname: "[Vorname]",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* Bewusst deterministisch (fester Seed): Die Zahlen bleiben über Reloads,
|
||||
* Screenshots und Abstimmungsrunden identisch. Für die echte Software wird
|
||||
* diese Datei durch Entity-Builder-Queries ersetzt — das Interface `Datensatz`
|
||||
* diese Datei durch Entity-Builder-Queries ersetzt, das Interface `Datensatz`
|
||||
* bleibt gleich.
|
||||
*
|
||||
* Betriebsprofil (fiktiv):
|
||||
@@ -32,7 +32,7 @@ import { HEUTE, addTage, toISO } from "../utils/dateUtils";
|
||||
|
||||
/* ------------------------------------------------------ Zufall mit Seed */
|
||||
|
||||
/** mulberry32 — kleiner, schneller PRNG mit reproduzierbarer Sequenz. */
|
||||
/** mulberry32: kleiner, schneller PRNG mit reproduzierbarer Sequenz. */
|
||||
function prng(seed: number) {
|
||||
let a = seed;
|
||||
return function random(): number {
|
||||
@@ -177,7 +177,7 @@ function baueKunden(anzahl: number): Kunde[] {
|
||||
umsatzGesamt: 0,
|
||||
auftraegeGesamt: 0,
|
||||
// Die meisten zahlen nahe am Zahlungsziel; ein kleiner Teil schleppt.
|
||||
// Gleichverteilt wäre jeder zweite Kunde auffällig — das wäre kein Betrieb,
|
||||
// Gleichverteilt wäre jeder zweite Kunde auffällig, das wäre kein Betrieb,
|
||||
// sondern ein Inkassofall.
|
||||
zahlungsmoralTage: gewichtet([
|
||||
{ wert: zahl(5, 14), gewicht: 42 },
|
||||
@@ -217,7 +217,7 @@ const WERT_SPANNEN: Record<string, [number, number]> = {
|
||||
"E-Check": [140, 390],
|
||||
};
|
||||
|
||||
/** Bezugsgröße der Förderquote — zentral in types.ts definiert. */
|
||||
/** Bezugsgröße der Förderquote: zentral in types.ts definiert. */
|
||||
const FOERDERFAEHIG = FOERDERFAEHIGE_KATEGORIEN;
|
||||
|
||||
/**
|
||||
@@ -225,12 +225,12 @@ const FOERDERFAEHIG = FOERDERFAEHIGE_KATEGORIEN;
|
||||
*
|
||||
* Die Verteilung bildet einen realistischen Trichter ab:
|
||||
* von 100 Anfragen werden rund 55 zum Angebot und rund 24 zum Auftrag.
|
||||
* Verlorene Vorgänge merken sich, auf welcher Stufe sie abgesprungen sind —
|
||||
* Verlorene Vorgänge merken sich, auf welcher Stufe sie abgesprungen sind,
|
||||
* sonst zeigt der Trichter oben zu wenig und unten zu viel.
|
||||
*/
|
||||
function ergebnisFuerAlter(
|
||||
tageAlt: number,
|
||||
/** Große Vorhaben werden angeboten und oft verglichen — kleine kaum. */
|
||||
/** Große Vorhaben werden angeboten und oft verglichen: kleine kaum. */
|
||||
grossauftrag: boolean
|
||||
): {
|
||||
status: AuftragStatus;
|
||||
@@ -297,7 +297,7 @@ function ergebnisFuerAlter(
|
||||
|
||||
/**
|
||||
* Leistungsart innerhalb eines Gewerks.
|
||||
* Wartung, Notdienst und E-Check sind im Alltag die häufigsten Aufträge —
|
||||
* Wartung, Notdienst und E-Check sind im Alltag die häufigsten Aufträge,
|
||||
* gleichverteilt zu ziehen würde den Durchschnittswert massiv verzerren.
|
||||
*/
|
||||
function kategorieFuer(gewerk: Gewerk): string {
|
||||
@@ -451,7 +451,7 @@ function baueRechnungen(auftraege: Auftrag[]): Rechnung[] {
|
||||
let bezahltAm: string | null = null;
|
||||
|
||||
if (gestellt > HEUTE) {
|
||||
// Rechnung existiert noch nicht — z. B. Schlussrechnung eines laufenden Auftrags.
|
||||
// Rechnung existiert noch nicht: z. B. Schlussrechnung eines laufenden Auftrags.
|
||||
status = "entwurf";
|
||||
} else if (ueberfaelligTage < 0) {
|
||||
// Noch innerhalb des Zahlungsziels; ein Teil zahlt sofort.
|
||||
@@ -473,7 +473,7 @@ function baueRechnungen(auftraege: Auftrag[]): Rechnung[] {
|
||||
status = "ueberfaellig";
|
||||
}
|
||||
} else if (ueberfaelligTage <= 120) {
|
||||
// Mahnstufe erreicht — der Großteil zahlt spätestens jetzt.
|
||||
// Mahnstufe erreicht, der Großteil zahlt spätestens jetzt.
|
||||
if (rnd() < 0.94) {
|
||||
status = "bezahlt";
|
||||
bezahltAm = toISO(addTage(faellig, zahl(-8, 70)));
|
||||
@@ -489,7 +489,7 @@ function baueRechnungen(auftraege: Auftrag[]): Rechnung[] {
|
||||
status = "ueberfaellig";
|
||||
}
|
||||
} else {
|
||||
// Älter als sieben Monate: eingebracht oder ausgebucht — in beiden
|
||||
// Älter als sieben Monate: eingebracht oder ausgebucht, in beiden
|
||||
// Fällen steht die Forderung nicht mehr in der offenen Liste.
|
||||
status = "bezahlt";
|
||||
bezahltAm = toISO(addTage(faellig, zahl(-8, 150)));
|
||||
|
||||
@@ -18,7 +18,7 @@ export const GEWERKE = [
|
||||
] as const;
|
||||
export type Gewerk = (typeof GEWERKE)[number];
|
||||
|
||||
/** Leistungsarten je Gewerk — Grundlage für "Umsatz nach Kategorie". */
|
||||
/** Leistungsarten je Gewerk: Grundlage für "Umsatz nach Kategorie". */
|
||||
export const KATEGORIEN: Record<Gewerk, string[]> = {
|
||||
Heizung: ["Wärmepumpe", "Gastherme", "Heizkörper", "Wartung Heizung"],
|
||||
Sanitär: ["Bad-Sanierung", "Rohrleitungen", "Armaturen", "Notdienst"],
|
||||
@@ -65,7 +65,7 @@ export interface Kunde {
|
||||
ort: string;
|
||||
angelegtAm: string;
|
||||
letzterAuftragAm: string | null;
|
||||
/** Aggregiert aus Rechnungen — wird in seed.ts nachberechnet. */
|
||||
/** Aggregiert aus Rechnungen: wird in seed.ts nachberechnet. */
|
||||
umsatzGesamt: number;
|
||||
auftraegeGesamt: number;
|
||||
zahlungsmoralTage: number;
|
||||
@@ -94,7 +94,7 @@ export const AUFTRAG_STATUS_LABEL: Record<AuftragStatus, string> = {
|
||||
|
||||
/**
|
||||
* Leistungsarten, für die BAFA-/KfW-Förderung in Frage kommt.
|
||||
* Bezugsgröße der Förderquote — sonst rechnet man Wartungsaufträge
|
||||
* Bezugsgröße der Förderquote, sonst rechnet man Wartungsaufträge
|
||||
* in eine Quote hinein, die für sie gar nicht gilt.
|
||||
*/
|
||||
export const FOERDERFAEHIGE_KATEGORIEN = new Set([
|
||||
@@ -137,7 +137,7 @@ export interface Auftrag {
|
||||
/**
|
||||
* Bei verlorenen Vorgängen: die zuletzt erreichte Trichterstufe.
|
||||
* Ohne dieses Feld müsste der Trichter raten, wo ein Vorgang abgesprungen
|
||||
* ist — und würde die oberen Stufen systematisch zu klein zeigen.
|
||||
* ist, und würde die oberen Stufen systematisch zu klein zeigen.
|
||||
*/
|
||||
verlorenAb: AuftragStatus | null;
|
||||
/**
|
||||
|
||||
@@ -9,8 +9,8 @@ import type { Datensatz, FilterState } from "../data/types";
|
||||
/**
|
||||
* Liefert den auf die aktive Filterauswahl zugeschnittenen Datensatz.
|
||||
*
|
||||
* `daten` — gefiltert nach den auf dieser Seite wirksamen Dimensionen
|
||||
* `filter` — die tatsächlich wirksamen Filter (irrelevante sind neutralisiert)
|
||||
* `daten`: gefiltert nach den auf dieser Seite wirksamen Dimensionen
|
||||
* `filter`, die tatsächlich wirksamen Filter (irrelevante sind neutralisiert)
|
||||
*
|
||||
* Der Zeitraum steckt bewusst nur im Filter, nicht im Datensatz: Manche
|
||||
* Kennzahlen sind zeitraumbezogen (Umsatz), andere stichtagsbezogen
|
||||
|
||||
@@ -6,7 +6,7 @@ import React from "react";
|
||||
* Ist das Fenster schmaler als der `md`-Breakpoint (768 px)?
|
||||
*
|
||||
* Nötig, weil unterhalb `md` die Seitenleiste über den Inhalt fährt statt ihn
|
||||
* zu verdrängen — und eine geschlossene Schublade muss dann auch für
|
||||
* zu verdrängen, und eine geschlossene Schublade muss dann auch für
|
||||
* Screenreader und Tastatur verschwinden (`aria-hidden`, `inert`). Über reine
|
||||
* CSS-Klassen ist das nicht zu haben: `max-md:` verändert die Darstellung,
|
||||
* nicht den Zugänglichkeitsbaum. Ohne Breitenprüfung wäre die Leiste auch auf
|
||||
@@ -14,7 +14,7 @@ import React from "react";
|
||||
*
|
||||
* `useSyncExternalStore` statt Effekt plus State: `matchMedia` ist genau so
|
||||
* ein externer Speicher, und beim Server-Rendering liefert die Momentaufnahme
|
||||
* `false` — die Leiste ist dort also sichtbar, was ohne JavaScript richtig ist.
|
||||
* `false`, die Leiste ist dort also sichtbar, was ohne JavaScript richtig ist.
|
||||
*/
|
||||
const ABFRAGE = "(max-width: 767px)";
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ export function AuftraegeSeite({ vm }: { vm: VMState<AuftraegeDaten> }) {
|
||||
id="auftraege-trichter"
|
||||
titel="Vertriebstrichter"
|
||||
untertitel="Angebotsgeschäft ab 6.000 €, nach erreichter Stufe"
|
||||
hinweis="Nur Vorhaben mit schriftlichem Angebot. Wartung und Notdienst gehen ohne Angebot direkt in die Ausführung — mitgezählt wäre der Trichter unten breiter als oben. Verlorene Vorgänge zählen bis zu der Stufe, auf der sie abgesprungen sind."
|
||||
hinweis="Nur Vorhaben mit schriftlichem Angebot. Wartung und Notdienst gehen ohne Angebot direkt in die Ausführung. Mitgezählt wäre der Trichter unten breiter als oben. Verlorene Vorgänge zählen bis zu der Stufe, auf der sie abgesprungen sind."
|
||||
>
|
||||
<Trichter stufen={d.trichter} formatWert={(w) => formatZahl(w)} />
|
||||
</ChartCard>
|
||||
@@ -176,7 +176,7 @@ export function AuftraegeSeite({ vm }: { vm: VMState<AuftraegeDaten> }) {
|
||||
<ChartCard
|
||||
id="auftraege-status"
|
||||
titel="Vorgänge nach Status"
|
||||
untertitel="Alle Vorgänge seit Beginn — unabhängig vom Zeitraumfilter"
|
||||
untertitel="Alle Vorgänge seit Beginn, unabhängig vom Zeitraumfilter"
|
||||
>
|
||||
<Donut
|
||||
daten={d.statusVerteilung}
|
||||
|
||||
@@ -25,7 +25,7 @@ import type { VMState } from "../view-models";
|
||||
import { ZEITRAUM_LABEL, type DashboardDaten, type KennzahlKachel } from "../view-models/dashboard.vm";
|
||||
|
||||
/**
|
||||
* Das Dashboard — der Begrüßungsbildschirm nach der Anmeldung.
|
||||
* Das Dashboard, der Begrüßungsbildschirm nach der Anmeldung.
|
||||
*
|
||||
* ## Was hier steht und warum
|
||||
*
|
||||
@@ -33,14 +33,14 @@ import { ZEITRAUM_LABEL, type DashboardDaten, type KennzahlKachel } from "../vie
|
||||
* „Warum ist das so?" (`docs/SCOPE-ANALYTICS-DASHBOARD.md`). Daraus folgt die
|
||||
* Reihenfolge auf dieser Seite, und zwar in dieser Richtung:
|
||||
*
|
||||
* 1. **Ansprache und Lage** — muss ich heute überhaupt etwas tun?
|
||||
* 2. **Vier Kennzahlen** — die Lage in Zahlen. Jede ist ein Link in die
|
||||
* 1. **Ansprache und Lage**: muss ich heute überhaupt etwas tun?
|
||||
* 2. **Vier Kennzahlen**, die Lage in Zahlen. Jede ist ein Link in die
|
||||
* Analyse, die sie erklärt.
|
||||
* 3. **Handlungsbedarf** — was zu tun ist. Laut Scope-Papier (3.2) gehört
|
||||
* 3. **Handlungsbedarf**, was zu tun ist. Laut Scope-Papier (3.2) gehört
|
||||
* diese Liste hierher und nicht auf die Analytics-Übersicht; das ist
|
||||
* hiermit umgesetzt.
|
||||
* 4. **Vier Diagramme** — die Zusammenfassung aus Analytics.
|
||||
* 5. **Reservierte Plätze** — was das Dashboard noch wird.
|
||||
* 4. **Vier Diagramme**, die Zusammenfassung aus Analytics.
|
||||
* 5. **Reservierte Plätze**, was das Dashboard noch wird.
|
||||
*
|
||||
* Absteigend nach Dringlichkeit, nicht nach Fachgebiet. Ein Dashboard, das
|
||||
* nach Fachgebieten sortiert ist, ist ein Inhaltsverzeichnis.
|
||||
@@ -49,7 +49,7 @@ import { ZEITRAUM_LABEL, type DashboardDaten, type KennzahlKachel } from "../vie
|
||||
*
|
||||
* Jede Kachel steht in `<Widget breite={…} start={…}>`. Diese beiden Zahlen
|
||||
* sind in der Software `widget.size.w` und `widget.position.x` und kommen dort
|
||||
* aus dem gespeicherten Layout — der Benutzer kann es bearbeiten. Hier sind
|
||||
* aus dem gespeicherten Layout, der Benutzer kann es bearbeiten. Hier sind
|
||||
* sie geschrieben. Das ist der einzige Unterschied; das Raster darunter ist
|
||||
* dasselbe (siehe `WidgetRaster.tsx`).
|
||||
*
|
||||
@@ -58,7 +58,7 @@ import { ZEITRAUM_LABEL, type DashboardDaten, type KennzahlKachel } from "../vie
|
||||
* Kein seitenweiter Filter. In Analytics wirkt eine Filterleiste auf alle
|
||||
* Karten; im Dashboard trägt jede Kachel ihren eigenen Zeitraum und sagt ihn
|
||||
* an (P11: „Edit = nur Widget-Filter/Zeitraum"). Eine Filterleiste über
|
||||
* dieser Seite wäre die bequemere Lösung und der falsche Weg — sie würde die
|
||||
* dieser Seite wäre die bequemere Lösung und der falsche Weg, sie würde die
|
||||
* Kacheln beim Einbau alle wieder auseinandernehmen.
|
||||
*
|
||||
* Auch keine Rollenumschaltung. Der Prototyp gestaltet die Sicht der
|
||||
@@ -98,7 +98,7 @@ export function DashboardSeite({ vm }: { vm: VMState<DashboardDaten> }) {
|
||||
<Widget breite={12} start={0}>
|
||||
<WidgetKachel
|
||||
titel="Handlungsbedarf"
|
||||
untertitel="Sortiert nach Dringlichkeit — jede Zeile führt in die zugehörige Arbeitsliste"
|
||||
untertitel="Sortiert nach Dringlichkeit, jede Zeile führt in die zugehörige Arbeitsliste"
|
||||
domaene="general"
|
||||
zeitraum="stichtag"
|
||||
>
|
||||
@@ -139,7 +139,7 @@ export function DashboardSeite({ vm }: { vm: VMState<DashboardDaten> }) {
|
||||
<WidgetKachel
|
||||
titel="Umsatz nach Gewerk"
|
||||
untertitel="Zahlungseingang der letzten 30 Tage"
|
||||
hinweis="Fünf Gewerke in fester Rangfolge — dieselbe Kodierung wie auf der Finanzseite."
|
||||
hinweis="Fünf Gewerke in fester Rangfolge: dieselbe Kodierung wie auf der Finanzseite."
|
||||
domaene="finance"
|
||||
zeitraum="30_tage"
|
||||
absprung="/analytics/finanzen"
|
||||
@@ -181,12 +181,12 @@ export function DashboardSeite({ vm }: { vm: VMState<DashboardDaten> }) {
|
||||
</Widget>
|
||||
|
||||
{/* ────────────────────────────────────────────────────────────────
|
||||
HIER GEHT ES WEITER — an dieser Stelle, nicht woanders.
|
||||
HIER GEHT ES WEITER: an dieser Stelle, nicht woanders.
|
||||
|
||||
Das Dashboard zeigt heute nur Auswertungen. Es wird mehr: eigene
|
||||
Aufgaben, Termine des Tages, offene HR-Anträge. Alle drei stehen
|
||||
im Designer-Brief P11 und haben in HiveGrid bereits ihre
|
||||
Widget-Domäne (`tasks`, `schedule`, `hr`) — sie brauchen kein
|
||||
Widget-Domäne (`tasks`, `schedule`, `hr`): sie brauchen kein
|
||||
neues Raster, nur Daten.
|
||||
|
||||
Konkret kommen sie als drei weitere `<Widget breite={4}>` mit
|
||||
@@ -213,7 +213,7 @@ export function DashboardSeite({ vm }: { vm: VMState<DashboardDaten> }) {
|
||||
/**
|
||||
* Eine Kennzahl als Kachel.
|
||||
*
|
||||
* Es ist dieselbe `KPICard` wie in Analytics — dieselbe Anatomie, dieselben
|
||||
* Es ist dieselbe `KPICard` wie in Analytics: dieselbe Anatomie, dieselben
|
||||
* Regeln zu Trend und Farbe. Neu ist nur, dass sie ein Link ist: Auf dem
|
||||
* Dashboard ist jede Zahl ein Absprung in die Analyse, die sie erklärt. Genau
|
||||
* so macht es die Software (`widget.href` legt den Link um die `StatsCard`).
|
||||
@@ -236,7 +236,7 @@ function KennzahlWidget({
|
||||
<Link
|
||||
href={kachel.href}
|
||||
className="block h-full rounded-lg no-underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2"
|
||||
aria-label={`${kachel.titel}: ${wert} — zur Analyse`}
|
||||
aria-label={`${kachel.titel}: ${wert}. Zur Analyse`}
|
||||
>
|
||||
<KPICard
|
||||
label={kachel.titel}
|
||||
|
||||
@@ -179,7 +179,7 @@ export function FinanzenSeite({ vm }: { vm: VMState<FinanzenDaten> }) {
|
||||
id="finanzen-status"
|
||||
titel="Offener Bestand nach Status"
|
||||
untertitel="Wie viel des offenen Bestands bereits überfällig ist"
|
||||
hinweis="Bezugsgröße ist ausschließlich der offene Bestand zum Stichtag. Bezahlte Rechnungen sind nicht enthalten — sie würden über 90 % des Rings belegen und die Aussage erdrücken."
|
||||
hinweis="Bezugsgröße ist ausschließlich der offene Bestand zum Stichtag. Bezahlte Rechnungen sind nicht enthalten, sie würden über 90 % des Rings belegen und die Aussage erdrücken."
|
||||
>
|
||||
<Donut
|
||||
daten={d.offenNachStatus}
|
||||
@@ -239,7 +239,7 @@ export function FinanzenSeite({ vm }: { vm: VMState<FinanzenDaten> }) {
|
||||
<ChartCard
|
||||
id="finanzen-offene-liste"
|
||||
titel={`Offene Rechnungen (${d.posten.anzahl})`}
|
||||
untertitel="Sortiert nach Fälligkeit — die Arbeitsliste für den Mahnlauf"
|
||||
untertitel="Sortiert nach Fälligkeit, die Arbeitsliste für den Mahnlauf"
|
||||
>
|
||||
<DataTable
|
||||
zeilen={d.posten.liste}
|
||||
|
||||
@@ -60,20 +60,20 @@ export function KomponentenSeite() {
|
||||
<>
|
||||
<PageHeader
|
||||
titel="Komponenten & Regeln"
|
||||
beschreibung="Jeder Baustein einzeln und in allen Zuständen — die Seite zum Draufzeigen in der Abstimmung."
|
||||
beschreibung="Jeder Baustein einzeln und in allen Zuständen: die Seite zum Draufzeigen in der Abstimmung."
|
||||
/>
|
||||
|
||||
<PageContainer>
|
||||
<Abschnitt
|
||||
titel="Warum diese Seite existiert"
|
||||
text="Auf den Analyse-Seiten sieht man von jedem Baustein immer nur den einen Zustand, den die Daten gerade hergeben — die Kennzahlenkarte mit steigendem Trend, das Diagramm mit Werten darin. Die anderen Zustände existieren trotzdem und müssen mitgestaltet werden. Diese Seite zeigt sie alle nebeneinander, damit man in der Abstimmung darauf zeigen kann, ohne erst einen Filter zu suchen, der den passenden Fall herstellt."
|
||||
text="Auf den Analyse-Seiten sieht man von jedem Baustein immer nur den einen Zustand, den die Daten gerade hergeben: die Kennzahlenkarte mit steigendem Trend, das Diagramm mit Werten darin. Die anderen Zustände existieren trotzdem und müssen mitgestaltet werden. Diese Seite zeigt sie alle nebeneinander, damit man in der Abstimmung darauf zeigen kann, ohne erst einen Filter zu suchen, der den passenden Fall herstellt."
|
||||
/>
|
||||
<Hinweis text="Diese Seite ist kein Produktfeature. Sie gehört weder zu Dashboard noch zu Analytics, steht in der Seitenleiste unter dem Trenner und wandert beim Einbau nicht mit. Für die Umsetzung selbst sind die Datei-Pfade unter jedem Abschnitt und docs/DESIGN-SYSTEM.md maßgeblich — beides steht im Repo und braucht diese Seite nicht." />
|
||||
<Hinweis text="Diese Seite ist kein Produktfeature. Sie gehört weder zu Dashboard noch zu Analytics, steht in der Seitenleiste unter dem Trenner und wandert beim Einbau nicht mit. Für die Umsetzung selbst sind die Datei-Pfade unter jedem Abschnitt und docs/DESIGN-SYSTEM.md maßgeblich. Beides steht im Repo und braucht diese Seite nicht." />
|
||||
|
||||
<ChartCard
|
||||
id="komp-farben"
|
||||
titel="Farbpalette"
|
||||
untertitel="IBM Carbon, kategoriale Palette (14 Farben) — Reihenfolge nie umsortieren"
|
||||
untertitel="IBM Carbon, kategoriale Palette (14 Farben): Reihenfolge nie umsortieren"
|
||||
hinweis="Die Reihenfolge ist auf Unterscheidbarkeit optimiert. Farben immer von vorne vergeben, nicht nach Geschmack auswählen."
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
@@ -146,7 +146,7 @@ export function KomponentenSeite() {
|
||||
<ChartCard
|
||||
id="komp-area"
|
||||
titel="Flächendiagramm mit Vergleich"
|
||||
untertitel="components/charts/AreaVerlauf.tsx — für Zeitverläufe"
|
||||
untertitel="components/charts/AreaVerlauf.tsx: für Zeitverläufe"
|
||||
className="xl:col-span-2"
|
||||
legende={
|
||||
<ChartLegende
|
||||
@@ -169,7 +169,7 @@ export function KomponentenSeite() {
|
||||
<ChartCard
|
||||
id="komp-donut"
|
||||
titel="Ringdiagramm"
|
||||
untertitel="components/charts/Donut.tsx — für Anteile"
|
||||
untertitel="components/charts/Donut.tsx: für Anteile"
|
||||
>
|
||||
<Donut daten={BEISPIEL_KATEGORIEN} formatWert={formatEuroKompakt} hoehe={200} />
|
||||
</ChartCard>
|
||||
@@ -179,7 +179,7 @@ export function KomponentenSeite() {
|
||||
<ChartCard
|
||||
id="komp-balken"
|
||||
titel="Säulendiagramm"
|
||||
untertitel="components/charts/BalkenVertikal.tsx — Mengen mit Nulllinie"
|
||||
untertitel="components/charts/BalkenVertikal.tsx: Mengen mit Nulllinie"
|
||||
>
|
||||
<BalkenVertikal
|
||||
daten={BEISPIEL_REIHE.map((p) => ({ label: p.label, value: p.value / 1000 }))}
|
||||
@@ -192,7 +192,7 @@ export function KomponentenSeite() {
|
||||
<ChartCard
|
||||
id="komp-hbalken"
|
||||
titel="Balkendiagramm (liegend)"
|
||||
untertitel="components/charts/BalkenHorizontal.tsx — Rangfolgen"
|
||||
untertitel="components/charts/BalkenHorizontal.tsx: Rangfolgen"
|
||||
>
|
||||
<BalkenHorizontal
|
||||
daten={BEISPIEL_KATEGORIEN}
|
||||
@@ -204,7 +204,7 @@ export function KomponentenSeite() {
|
||||
<ChartCard
|
||||
id="komp-trichter"
|
||||
titel="Trichter"
|
||||
untertitel="components/charts/Trichter.tsx — Stufen mit Übergangsquote"
|
||||
untertitel="components/charts/Trichter.tsx: Stufen mit Übergangsquote"
|
||||
>
|
||||
<Trichter
|
||||
stufen={[
|
||||
@@ -223,7 +223,7 @@ export function KomponentenSeite() {
|
||||
<ChartCard
|
||||
id="komp-auslastung"
|
||||
titel="Auslastungsbalken"
|
||||
untertitel="components/charts/AuslastungsBalken.tsx — mit Ampel und 100-%-Marke"
|
||||
untertitel="components/charts/AuslastungsBalken.tsx: mit Ampel und 100-%-Marke"
|
||||
>
|
||||
<AuslastungsBalken
|
||||
zeilen={[
|
||||
@@ -238,7 +238,7 @@ export function KomponentenSeite() {
|
||||
<ChartCard
|
||||
id="komp-alerts"
|
||||
titel="Handlungsbedarf"
|
||||
untertitel="components/cards/AlertList.tsx — nach Dringlichkeit sortiert"
|
||||
untertitel="components/cards/AlertList.tsx: nach Dringlichkeit sortiert"
|
||||
>
|
||||
<AlertList
|
||||
eintraege={[
|
||||
@@ -273,7 +273,7 @@ export function KomponentenSeite() {
|
||||
<ChartCard
|
||||
id="komp-leer"
|
||||
titel="Leerzustand"
|
||||
untertitel="components/charts/ChartLeer.tsx — greift, wenn Filter alles wegschneiden"
|
||||
untertitel="components/charts/ChartLeer.tsx: greift, wenn Filter alles wegschneiden"
|
||||
>
|
||||
<ChartLeer hoehe={180} />
|
||||
</ChartCard>
|
||||
@@ -281,12 +281,12 @@ export function KomponentenSeite() {
|
||||
|
||||
<Abschnitt
|
||||
titel="Seitenzustände"
|
||||
text="features/dashboard/components/layout/SeitenZustand.tsx — die vier Zustände außer „fertig“. Sie sind in HiveGrid Pflicht (VMState: idle · loading · ready · empty · error) und im laufenden Prototyp nie zu sehen, weil die Demo-Daten immer liefern. Gestaltet werden müssen sie trotzdem: „lädt“, „nichts gefunden“ und „kaputt“ sind drei verschiedene Aussagen und dürfen nicht gleich aussehen."
|
||||
text="features/dashboard/components/layout/SeitenZustand.tsx: die vier Zustände außer „fertig“. Sie sind in HiveGrid Pflicht (VMState: idle · loading · ready · empty · error) und im laufenden Prototyp nie zu sehen, weil die Demo-Daten immer liefern. Gestaltet werden müssen sie trotzdem: „lädt“, „nichts gefunden“ und „kaputt“ sind drei verschiedene Aussagen und dürfen nicht gleich aussehen."
|
||||
/>
|
||||
<div className="grid grid-cols-1 gap-4 xl:grid-cols-3">
|
||||
{([
|
||||
["Lädt", { status: "loading" } as VMState<unknown>],
|
||||
["Leer — Filter schneiden alles weg", { status: "empty" } as VMState<unknown>],
|
||||
["Leer: Filter schneiden alles weg", { status: "empty" } as VMState<unknown>],
|
||||
[
|
||||
"Fehler",
|
||||
{
|
||||
@@ -314,7 +314,7 @@ export function KomponentenSeite() {
|
||||
|
||||
<Abschnitt
|
||||
titel="Dashboard-Kacheln"
|
||||
text="features/dashboard/components/dashboard/ — das Dashboard benutzt nicht ChartCard, sondern einen eigenen Kachelrahmen. Gleiche Kante, gleiche Abstände, aber ohne das ⋯-Menü von Analytics; dafür mit Domänen-Icon, eigenem Zeitraum und Absprung. Der Grund steht in docs/SCOPE-ANALYTICS-DASHBOARD.md: In Analytics gilt ein Filter für alle Karten, im Dashboard trägt jede Kachel ihren eigenen."
|
||||
text="features/dashboard/components/dashboard/: das Dashboard benutzt nicht ChartCard, sondern einen eigenen Kachelrahmen. Gleiche Kante, gleiche Abstände, aber ohne das ⋯-Menü von Analytics; dafür mit Domänen-Icon, eigenem Zeitraum und Absprung. Der Grund steht in docs/SCOPE-ANALYTICS-DASHBOARD.md: In Analytics gilt ein Filter für alle Karten, im Dashboard trägt jede Kachel ihren eigenen."
|
||||
/>
|
||||
{/* Wie bei den Seitenzuständen: Die Begrüßung trägt ihren eigenen
|
||||
Seitenrand, weil sie im Dashboard direkt auf der Seite steht. In der
|
||||
@@ -351,16 +351,16 @@ export function KomponentenSeite() {
|
||||
</div>
|
||||
|
||||
<Abschnitt
|
||||
titel="Reservierte Kacheln — was noch ins Dashboard kommt"
|
||||
text="features/dashboard/components/dashboard/ReservierteKachel.tsx — das Dashboard zeigt heute nur Auswertungen. Diese drei Bereiche kommen dazu; alle drei stehen im Designer-Brief P11 und haben in HiveGrid bereits ihre Widget-Domäne (tasks, schedule, hr). Sie brauchen kein neues Raster, nur Daten."
|
||||
titel="Reservierte Kacheln: was noch ins Dashboard kommt"
|
||||
text="features/dashboard/components/dashboard/ReservierteKachel.tsx: das Dashboard zeigt heute nur Auswertungen. Diese drei Bereiche kommen dazu; alle drei stehen im Designer-Brief P11 und haben in HiveGrid bereits ihre Widget-Domäne (tasks, schedule, hr). Sie brauchen kein neues Raster, nur Daten."
|
||||
/>
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
|
||||
{RESERVIERTE_BEREICHE.map((bereich) => (
|
||||
<ReservierteKachel key={bereich.key} bereich={bereich} />
|
||||
))}
|
||||
</div>
|
||||
<Hinweis text="Diese Kacheln stehen hier und nicht auf dem Dashboard. Ein Begrüßungsbildschirm ist kein Ort für Ankündigungen — wer sich morgens anmeldet, will seine Lage sehen und nicht lesen, was später einmal kommt. Für den Einbau ist die Stelle im Raster als Kommentar in DashboardSeite.tsx markiert; der Zuschnitt steht in docs/SCOPE-ANALYTICS-DASHBOARD.md, Abschnitt 3a." />
|
||||
<Hinweis text="Sie zeigen bewusst keine Zahl. „2 neue Anträge“ aus erfundenen Daten liest sich in einer Abnahme wie eine fertige Funktion — und irgendwann fragt jemand, warum die Zahl nie hochzählt. Die gestrichelte Kante sagt dasselbe ehrlicher: Hier kommt etwas hin, die Fläche ist dafür da."/>
|
||||
<Hinweis text="Diese Kacheln stehen hier und nicht auf dem Dashboard. Ein Begrüßungsbildschirm ist kein Ort für Ankündigungen. Wer sich morgens anmeldet, will seine Lage sehen und nicht lesen, was später einmal kommt. Für den Einbau ist die Stelle im Raster als Kommentar in DashboardSeite.tsx markiert; der Zuschnitt steht in docs/SCOPE-ANALYTICS-DASHBOARD.md, Abschnitt 3a." />
|
||||
<Hinweis text="Sie zeigen bewusst keine Zahl. „2 neue Anträge“ aus erfundenen Daten liest sich in einer Abnahme wie eine fertige Funktion, und irgendwann fragt jemand, warum die Zahl nie hochzählt. Die gestrichelte Kante sagt dasselbe ehrlicher: Hier kommt etwas hin, die Fläche ist dafür da."/>
|
||||
|
||||
<ChartCard
|
||||
id="komp-badges"
|
||||
@@ -405,13 +405,13 @@ export function KomponentenSeite() {
|
||||
{[
|
||||
"Nur horizontale Gridlines in #e0e0e0. Vertikale Linien lenken vom Verlauf ab.",
|
||||
"Achsenbeschriftungen 12 px in #525252, keine Tick-Marks, Achsenlinie #8d8d8d.",
|
||||
"Ein Tooltip für alle Charts: weiße Fläche #ffffff, Text #161616, Schatten statt Rahmen — seit Carbon v11 hell, nicht mehr dunkel.",
|
||||
"Ein Tooltip für alle Charts: weiße Fläche #ffffff, Text #161616, Schatten statt Rahmen. Seit Carbon v11 ist er hell, nicht mehr dunkel.",
|
||||
"Maximal fünf Kategorien je Chart, alles darüber wird zu „Sonstige“ zusammengefasst.",
|
||||
"Balkenabstand mindestens halbe Balkenbreite; Balken nie breiter als 48 px.",
|
||||
"Zahlen rechtsbündig und in Tabellenziffern, Text linksbündig.",
|
||||
"Karten: 16 px Innenabstand, 16 px Abstand zwischen Karten — innen nie größer als außen.",
|
||||
"Karten: 16 px Innenabstand, 16 px Abstand zwischen Karten; innen nie größer als außen.",
|
||||
"Jede Karte braucht einen Leerzustand. Filter können jedes Chart leer laufen lassen.",
|
||||
"Karten einer Zeile nur strecken, wenn sie dieselbe Bauart haben. Ein Diagramm mit fester Höhe neben einer Balkenliste bleibt oben bündig — sonst wird aus einer Karte mit einem Balken eine 427 px hohe Fläche.",
|
||||
"Karten einer Zeile nur strecken, wenn sie dieselbe Bauart haben. Ein Diagramm mit fester Höhe neben einer Balkenliste bleibt oben bündig, sonst wird aus einer Karte mit einem Balken eine 427 px hohe Fläche.",
|
||||
].map((regel) => (
|
||||
<li key={regel} className="flex gap-2.5 text-[13px] leading-relaxed text-text-neutral-secondary">
|
||||
<span className="mt-1.5 h-1.5 w-1.5 flex-none rounded-full bg-neutral-400" />
|
||||
|
||||
@@ -164,7 +164,7 @@ export function TeamSeite({ vm }: { vm: VMState<TeamDaten> }) {
|
||||
id="team-abschluesse"
|
||||
titel="Abschlüsse je Mitarbeiter"
|
||||
untertitel="Anzahl im gewählten Zeitraum, dahinter der zugehörige Auftragswert"
|
||||
hinweis="Der Balken zeigt die Anzahl — sie ist die vergleichbare Größe. Der Auftragswert steht daneben, ist aber ausdrücklich kein Leistungsvergleich: Ein Wärmepumpen-Auftrag ist um ein Vielfaches größer als eine Wartung."
|
||||
hinweis="Der Balken zeigt die Anzahl, sie ist die vergleichbare Größe. Der Auftragswert steht daneben, ist aber ausdrücklich kein Leistungsvergleich: Ein Wärmepumpen-Auftrag ist um ein Vielfaches größer als eine Wartung."
|
||||
>
|
||||
<BalkenHorizontal
|
||||
daten={d.aktive.map((k) => ({
|
||||
|
||||
@@ -126,7 +126,7 @@ export function UebersichtSeite({ vm }: { vm: VMState<UebersichtDaten> }) {
|
||||
<ChartCard
|
||||
id="uebersicht-handlungsbedarf"
|
||||
titel="Handlungsbedarf"
|
||||
untertitel="Sortiert nach Dringlichkeit — jede Zeile führt in die zugehörige Arbeitsliste"
|
||||
untertitel="Sortiert nach Dringlichkeit, jede Zeile führt in die zugehörige Arbeitsliste"
|
||||
>
|
||||
<AlertList eintraege={d.alerts} spalten={2} />
|
||||
</ChartCard>
|
||||
@@ -171,7 +171,7 @@ export function UebersichtSeite({ vm }: { vm: VMState<UebersichtDaten> }) {
|
||||
id="uebersicht-gewerke"
|
||||
titel="Umsatz nach Gewerk"
|
||||
untertitel="Zahlungseingang im gewählten Zeitraum"
|
||||
hinweis="Fünf Gewerke in fester Rangfolge. Dieselbe Kodierung wie auf der Finanzseite — ein Ring und eine Balkenliste für dieselbe Zahl wären zwei Bilder für einen Sachverhalt."
|
||||
hinweis="Fünf Gewerke in fester Rangfolge. Dieselbe Kodierung wie auf der Finanzseite. Ein Ring und eine Balkenliste für dieselbe Zahl wären zwei Bilder für einen Sachverhalt."
|
||||
>
|
||||
<BalkenHorizontal
|
||||
daten={d.gewerkUmsatz}
|
||||
@@ -218,7 +218,7 @@ export function UebersichtSeite({ vm }: { vm: VMState<UebersichtDaten> }) {
|
||||
spalten={auftragSpalten}
|
||||
proSeite={6}
|
||||
suchFelder={(a) => `${a.titel} ${a.nummer} ${a.gewerk}`}
|
||||
leerText="Kein Auftrag ist über Termin — sehr gut."
|
||||
leerText="Kein Auftrag ist über Termin: sehr gut."
|
||||
/>
|
||||
</ChartCard>
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import { AuftraegeSeite } from "../pages/AuftraegeSeite";
|
||||
* }
|
||||
*
|
||||
* Hier steht statt des Loaders der Filter-Store. Beim Einbau wird diese Datei
|
||||
* ersetzt, nicht angepasst — die Seite darunter bleibt unberührt.
|
||||
* ersetzt, nicht angepasst, die Seite darunter bleibt unberührt.
|
||||
*/
|
||||
export function AuftraegeRoute() {
|
||||
const { daten, filter, gesamt } = useAnalyseDaten("auftraege");
|
||||
|
||||
@@ -15,7 +15,7 @@ import { DashboardSeite } from "../pages/DashboardSeite";
|
||||
*
|
||||
* `HEUTE` statt `new Date()`: Der Prototyp rechnet gegen einen festen
|
||||
* Stichtag, damit die Zahlen über Reloads und Screenshots hinweg gleich
|
||||
* bleiben — und damit Server- und Client-Rendering dieselbe Begrüßung
|
||||
* bleiben, und damit Server- und Client-Rendering dieselbe Begrüßung
|
||||
* ergeben. In HiveGrid Cloud steht an dieser Stelle die echte Uhr, und der
|
||||
* Benutzer kommt aus `requireMember`:
|
||||
*
|
||||
@@ -24,7 +24,7 @@ import { DashboardSeite } from "../pages/DashboardSeite";
|
||||
* return { vm: buildDashboardVM(await loadData(), member, new Date()) };
|
||||
* }
|
||||
*
|
||||
* Beim Einbau wird diese Datei ersetzt, nicht angepasst — die Seite darunter
|
||||
* Beim Einbau wird diese Datei ersetzt, nicht angepasst, die Seite darunter
|
||||
* bleibt unberührt.
|
||||
*/
|
||||
export function DashboardRoute() {
|
||||
|
||||
@@ -18,7 +18,7 @@ import { FinanzenSeite } from "../pages/FinanzenSeite";
|
||||
* }
|
||||
*
|
||||
* Hier steht statt des Loaders der Filter-Store. Beim Einbau wird diese Datei
|
||||
* ersetzt, nicht angepasst — die Seite darunter bleibt unberührt.
|
||||
* ersetzt, nicht angepasst, die Seite darunter bleibt unberührt.
|
||||
*/
|
||||
export function FinanzenRoute() {
|
||||
const { daten, filter, gesamt } = useAnalyseDaten("finanzen");
|
||||
|
||||
@@ -18,7 +18,7 @@ import { KundenSeite } from "../pages/KundenSeite";
|
||||
* }
|
||||
*
|
||||
* Hier steht statt des Loaders der Filter-Store. Beim Einbau wird diese Datei
|
||||
* ersetzt, nicht angepasst — die Seite darunter bleibt unberührt.
|
||||
* ersetzt, nicht angepasst, die Seite darunter bleibt unberührt.
|
||||
*/
|
||||
export function KundenRoute() {
|
||||
const { daten, filter, gesamt } = useAnalyseDaten("kunden");
|
||||
|
||||
@@ -18,7 +18,7 @@ import { TeamSeite } from "../pages/TeamSeite";
|
||||
* }
|
||||
*
|
||||
* Hier steht statt des Loaders der Filter-Store. Beim Einbau wird diese Datei
|
||||
* ersetzt, nicht angepasst — die Seite darunter bleibt unberührt.
|
||||
* ersetzt, nicht angepasst, die Seite darunter bleibt unberührt.
|
||||
*/
|
||||
export function TeamRoute() {
|
||||
const { daten, filter, gesamt } = useAnalyseDaten("team");
|
||||
|
||||
@@ -18,7 +18,7 @@ import { UebersichtSeite } from "../pages/UebersichtSeite";
|
||||
* }
|
||||
*
|
||||
* Hier steht statt des Loaders der Filter-Store. Beim Einbau wird diese Datei
|
||||
* ersetzt, nicht angepasst — die Seite darunter bleibt unberührt.
|
||||
* ersetzt, nicht angepasst, die Seite darunter bleibt unberührt.
|
||||
*/
|
||||
export function UebersichtRoute() {
|
||||
const { daten, filter, gesamt } = useAnalyseDaten("uebersicht");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Zentrales Chart-Theme — abgeleitet aus IBM Carbon Data Visualization.
|
||||
* Zentrales Chart-Theme: abgeleitet aus IBM Carbon Data Visualization.
|
||||
*
|
||||
* Die Werte stammen aus dem ausgelieferten Stylesheet von @carbon/charts
|
||||
* (dist/styles.css, Light Theme):
|
||||
@@ -52,7 +52,7 @@ export const STATUS_FARBEN = {
|
||||
/**
|
||||
* Feste Farbe je Gewerk.
|
||||
*
|
||||
* Eine Kategorie behält ihre Farbe über alle Charts und Seiten hinweg —
|
||||
* Eine Kategorie behält ihre Farbe über alle Charts und Seiten hinweg,
|
||||
* sonst ist Heizung im Ring lila und im Balkendiagramm blau, und man muss
|
||||
* jede Legende neu lesen. Die Werte stammen aus der Carbon-Palette.
|
||||
*/
|
||||
@@ -118,15 +118,15 @@ export const STATUS_TEXT = {
|
||||
* keine unabhängigen Kategorien.
|
||||
*/
|
||||
export const RISIKO_RAMPE = [
|
||||
"#198038", // green 60 — im Zahlungsziel
|
||||
"#b28600", // yellow 50 — beobachten
|
||||
"#ba4e00", // orange 60 — mahnen
|
||||
"#da1e28", // red 60 — Abschreibungsrisiko
|
||||
"#198038", // green 60: im Zahlungsziel
|
||||
"#b28600", // yellow 50: beobachten
|
||||
"#ba4e00", // orange 60: mahnen
|
||||
"#da1e28", // red 60: Abschreibungsrisiko
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Badge-Töne: helle Fläche mit dunklem Text derselben Carbon-Familie.
|
||||
* Alle Paare erreichen auf ihrer Fläche mindestens 7:1 — Badges sind klein
|
||||
* Alle Paare erreichen auf ihrer Fläche mindestens 7:1: Badges sind klein
|
||||
* (11 px) und brauchen mehr Reserve als Fließtext.
|
||||
*/
|
||||
export const BADGE_TON = {
|
||||
@@ -138,9 +138,9 @@ export const BADGE_TON = {
|
||||
negativ: "bg-[#fff1f1] text-[#a2191f]",
|
||||
} as const;
|
||||
|
||||
/** Einzelserie (Umsatzverlauf o. Ä.) — Carbon Cyan 50. */
|
||||
/** Einzelserie (Umsatzverlauf o. Ä.): Carbon Cyan 50. */
|
||||
export const PRIMAER_FARBE = "#1192e8";
|
||||
/** Vergleichsserie (Vorjahr) — bewusst neutral, damit sie zurücktritt. */
|
||||
/** Vergleichsserie (Vorjahr): bewusst neutral, damit sie zurücktritt. */
|
||||
export const VERGLEICH_FARBE = "#8d8d8d";
|
||||
|
||||
export const CHART = {
|
||||
@@ -150,7 +150,7 @@ export const CHART = {
|
||||
textSekundaer: "#525252",
|
||||
// Seit Carbon v11 ist der Tooltip hell: --cds-layer-02 auf --cds-text-primary,
|
||||
// getrennt vom Untergrund allein durch den Schatten. Bis v10 war er dunkel
|
||||
// (#393939) — diese Angabe steht in vielen älteren Quellen noch so.
|
||||
// (#393939): diese Angabe steht in vielen älteren Quellen noch so.
|
||||
tooltipBg: "#ffffff",
|
||||
tooltipFg: "#161616",
|
||||
} as const;
|
||||
@@ -193,7 +193,7 @@ export const achseY = {
|
||||
tickLine: false,
|
||||
axisLine: false,
|
||||
tick: { fill: CHART.textSekundaer, fontSize: CHART_FONT.achse },
|
||||
// 58 px laut docs/DESIGN-SYSTEM.md §2 — reicht für "245 T€".
|
||||
// 58 px laut docs/DESIGN-SYSTEM.md §2: reicht für "245 T€".
|
||||
// Werte im Mio.-Bereich bekommen über achsenFormat() eine kurze Einheit.
|
||||
width: 58,
|
||||
} as const;
|
||||
@@ -216,7 +216,7 @@ export const DONUT = {
|
||||
|
||||
/**
|
||||
* Reduziert eine Kategorienliste auf MAX_KATEGORIEN und fasst den Rest
|
||||
* zu "Sonstige" zusammen — verhindert unlesbare Donuts mit 12 Segmenten.
|
||||
* zu "Sonstige" zusammen: verhindert unlesbare Donuts mit 12 Segmenten.
|
||||
*/
|
||||
export function gruppiereRest<T extends { name: string; value: number }>(
|
||||
daten: T[],
|
||||
@@ -235,7 +235,7 @@ export function gruppiereRest<T extends { name: string; value: number }>(
|
||||
/**
|
||||
* Rundes Achsenraster.
|
||||
*
|
||||
* Recharts leitet Ticks aus max/(n-1) ab und rundet den Rohschritt auf — bei
|
||||
* Recharts leitet Ticks aus max/(n-1) ab und rundet den Rohschritt auf, bei
|
||||
* 219 T€ ergibt das 0/55/110/165/220. Krumme Achsenwerte kosten den Leser
|
||||
* bei jedem Blick eine Rechenoperation. Deshalb Schrittweiten aus der
|
||||
* 1-2-2,5-5-Reihe erzwingen.
|
||||
@@ -283,15 +283,15 @@ export const MIN_TICK_ABSTAND = 12;
|
||||
*
|
||||
* **Viele Punkte.** Ueber MAX_ACHSENLABELS Werten wird fest jede n-te
|
||||
* Beschriftung gezeigt. Das ist eine Entscheidung ueber die Lesbarkeit der
|
||||
* Reihe und darf nicht von der Kartenbreite abhaengen — sonst zeigt dieselbe
|
||||
* Reihe und darf nicht von der Kartenbreite abhaengen, sonst zeigt dieselbe
|
||||
* Reihe auf zwei Bildschirmen verschiedene Stuetzstellen.
|
||||
*
|
||||
* **Wenige Punkte in einer schmalen Karte.** Zwoelf Monatsnamen brauchen rund
|
||||
* 340 px. Eine Drittelkarte hat bei 1400 px knapp 300 — dort standen "Aug",
|
||||
* 340 px. Eine Drittelkarte hat bei 1400 px knapp 300: dort standen "Aug",
|
||||
* "Sep" und "Okt" ohne Luecke aneinander. Diese Faelle haengen an der Breite
|
||||
* und nicht an der Datenmenge, deshalb entscheidet sie Recharts zur Laufzeit:
|
||||
* `preserveStartEnd` laesst weg, was nicht passt, und haelt dabei die erste
|
||||
* und letzte Beschriftung — ohne die waere der Zeitraum nicht mehr ablesbar.
|
||||
* und letzte Beschriftung, ohne die waere der Zeitraum nicht mehr ablesbar.
|
||||
*/
|
||||
export function achsenIntervall(anzahlPunkte: number): number | "preserveStartEnd" {
|
||||
return anzahlPunkte > MAX_ACHSENLABELS
|
||||
@@ -312,7 +312,7 @@ export function farbeFuer(name: string, index: number): string {
|
||||
return GEWERK_FARBEN[name] ?? STATUS_KATEGORIE_FARBEN[name] ?? farbe(index);
|
||||
}
|
||||
|
||||
/** Abgestufte Einfärbung für Trichterstufen — eine Farbe, abnehmende Sättigung. */
|
||||
/** Abgestufte Einfärbung für Trichterstufen, eine Farbe, abnehmende Sättigung. */
|
||||
export function trichterFarbe(index: number, gesamt: number): string {
|
||||
const anteil = gesamt > 1 ? index / (gesamt - 1) : 0;
|
||||
// Von Carbon Purple 70 nach Purple 40: gleiche Farbfamilie, klare Reihenfolge.
|
||||
|
||||
@@ -55,7 +55,7 @@ export function zeitfenster(preset: ZeitraumPreset): Zeitfenster {
|
||||
}
|
||||
}
|
||||
|
||||
/** Gleich langes Fenster direkt davor — Basis fuer Trend-Vergleiche. */
|
||||
/** Gleich langes Fenster direkt davor: Basis fuer Trend-Vergleiche. */
|
||||
export function vorperiode(preset: ZeitraumPreset): Zeitfenster {
|
||||
const { von, bis } = zeitfenster(preset);
|
||||
const dauer = bis.getTime() - von.getTime();
|
||||
|
||||
@@ -55,7 +55,7 @@ export function formatEuroAchse(value: number): string {
|
||||
/**
|
||||
* Achsenformat, einmal aus dem Maximum gewaehlt.
|
||||
*
|
||||
* formatEuroAchse entscheidet die Einheit pro Wert — dadurch steht "0 €"
|
||||
* formatEuroAchse entscheidet die Einheit pro Wert, dadurch steht "0 €"
|
||||
* neben "10 T€". Eine Achse traegt aber genau eine Einheit.
|
||||
*/
|
||||
export function achsenFormat(max: number): (wert: number) => string {
|
||||
@@ -90,7 +90,7 @@ export function formatStunden(value: number): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Ganze Tage mit korrektem Numerus — "1 Tag", "3 Tage".
|
||||
* Ganze Tage mit korrektem Numerus: "1 Tag", "3 Tage".
|
||||
* Ohne das steht in der Terminspalte "1 Tage ueber".
|
||||
*/
|
||||
export function tageLabel(anzahl: number, suffix = ""): string {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* View-Model der Auftragsseite.
|
||||
*
|
||||
* Reine Funktion über typisierten Eingangsdaten — kein React, kein Store, kein
|
||||
* Reine Funktion über typisierten Eingangsdaten, kein React, kein Store, kein
|
||||
* Zugriff auf `DATENSATZ`. Genau diese Grenze macht den Einbau billig: In der
|
||||
* Software ruft der Loader denselben Builder mit echten Daten auf, die
|
||||
* Screen-Komponente bleibt unverändert.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Das Dashboard ist **kein zweiter Analytics-Bereich**, sondern der
|
||||
* Begrüßungsbildschirm: „Wie ist meine Lage?" statt „Warum ist das so?".
|
||||
* Es zeigt heute ausschließlich Analytics-Kacheln, ist aber als Widget-Fläche
|
||||
* gebaut — andere Bereiche (Aufgaben, Termine, HR-Anträge) kommen später als
|
||||
* gebaut: andere Bereiche (Aufgaben, Termine, HR-Anträge) kommen später als
|
||||
* weitere Kacheln dazu, nicht als Umbau.
|
||||
*
|
||||
* Die Typen unten spiegeln `DashboardWidgetDomain`, `DashboardWidgetKind` und
|
||||
@@ -59,7 +59,7 @@ export interface KennzahlKachel {
|
||||
readonly domaene: WidgetDomaene;
|
||||
readonly titel: string;
|
||||
readonly wert: number;
|
||||
/** Wie der Wert zu schreiben ist — Formatierung ist Darstellung, nicht Modell. */
|
||||
/** Wie der Wert zu schreiben ist: Formatierung ist Darstellung, nicht Modell. */
|
||||
readonly formatAls: "euro" | "zahl" | "prozent";
|
||||
readonly zusatz: string;
|
||||
readonly zeitraum: WidgetZeitraum;
|
||||
@@ -89,7 +89,7 @@ export interface ReservierterBereich {
|
||||
readonly titel: string;
|
||||
readonly beschreibung: string;
|
||||
readonly icon: string;
|
||||
/** Woher die Daten kommen werden — als Verabredung, nicht als Versprechen. */
|
||||
/** Woher die Daten kommen werden, als Verabredung, nicht als Versprechen. */
|
||||
readonly quelle: string;
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ export interface DashboardDaten {
|
||||
readonly rolle: string;
|
||||
readonly datum: string;
|
||||
};
|
||||
/** Ein Satz zur Lage — was der Betrachter wissen muss, bevor er weiterliest. */
|
||||
/** Ein Satz zur Lage, was der Betrachter wissen muss, bevor er weiterliest. */
|
||||
readonly lage: string;
|
||||
readonly kennzahlen: KennzahlKachel[];
|
||||
readonly handlungsbedarf: AlertEintrag[];
|
||||
@@ -109,14 +109,14 @@ export interface DashboardDaten {
|
||||
readonly umsatzVorher: number;
|
||||
readonly gewerkUmsatz: ChartPunkt[];
|
||||
readonly trichter: ChartPunkt[];
|
||||
/** Sparkline unter der Umsatzkachel — dieselbe Reihe wie in Analytics. */
|
||||
/** Sparkline unter der Umsatzkachel: dieselbe Reihe wie in Analytics. */
|
||||
readonly sparkline: number[];
|
||||
readonly auslastung: { name: string; wert: number; zusatz: string }[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tageszeit-Anrede. Die Uhrzeit kommt von außen, damit diese Funktion rein
|
||||
* bleibt — der Prototyp reicht seinen festen Stichtag herein, die Software
|
||||
* bleibt, der Prototyp reicht seinen festen Stichtag herein, die Software
|
||||
* die echte Uhr.
|
||||
*/
|
||||
function anredeFuer(stunde: number): string {
|
||||
@@ -125,7 +125,7 @@ function anredeFuer(stunde: number): string {
|
||||
return "Guten Abend";
|
||||
}
|
||||
|
||||
/** Der angemeldete Benutzer — in der Software aus der Sitzung, hier eine Attrappe. */
|
||||
/** Der angemeldete Benutzer, in der Software aus der Sitzung, hier eine Attrappe. */
|
||||
export interface Nutzer {
|
||||
readonly vorname: string;
|
||||
readonly rolle: string;
|
||||
@@ -151,7 +151,7 @@ export function baueDashboardVM(
|
||||
const alerts = handlungsbedarf(daten);
|
||||
|
||||
// Die Anwendung duzt. Das ist eine Festlegung fuer den ganzen Prototyp und
|
||||
// nicht nur fuer diesen Satz — halb geduzt liest sich schlechter als
|
||||
// nicht nur fuer diesen Satz: halb geduzt liest sich schlechter als
|
||||
// durchgehend gesiezt.
|
||||
const kritisch = alerts.filter((a) => a.art === "kritisch").length;
|
||||
const lage = kritisch
|
||||
@@ -228,7 +228,7 @@ export function baueDashboardVM(
|
||||
sparkline: verlauf.slice(-10).map((p) => p.value),
|
||||
// Sechs Zeilen: mehr passt nicht in eine halbe Kachel, ohne dass die
|
||||
// Balken auf Haarbreite schrumpfen. Die vollständige Liste steht in
|
||||
// Analytics — deshalb führt die Kachel dorthin.
|
||||
// Analytics, deshalb führt die Kachel dorthin.
|
||||
auslastung: monteure.slice(0, 6).map((t) => ({
|
||||
name: t.mitarbeiter.name,
|
||||
wert: t.auslastungProzent,
|
||||
@@ -243,7 +243,7 @@ export function baueDashboardVM(
|
||||
* Alle drei sind im Designer-Brief P11 für die Rollen-Dashboards benannt
|
||||
* („Büro: … meine Tasks, Anrufe heute, HR-Anträge") und haben in HiveGrid
|
||||
* bereits ihre Widget-Domäne. Sie stehen hier, damit beim Einbau feststeht,
|
||||
* was noch dazukommt und wo — und damit niemand das Raster dafür umbaut.
|
||||
* was noch dazukommt und wo, und damit niemand das Raster dafür umbaut.
|
||||
*
|
||||
* Gezeigt werden sie in der Bausteingalerie unter `/komponenten`.
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Auswahlmöglichkeiten der Filterleiste.
|
||||
*
|
||||
* Reine Funktion über dem vollständigen Datensatz — bewusst **nicht** über den
|
||||
* Reine Funktion über dem vollständigen Datensatz: bewusst **nicht** über den
|
||||
* gefilterten. Sonst verschwindet eine Option, sobald man sie abwählt, und der
|
||||
* Filter lässt sich nicht mehr zurücknehmen.
|
||||
*
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* View-Model der Finanzseite.
|
||||
*
|
||||
* Reine Funktion über typisierten Eingangsdaten — kein React, kein Store, kein
|
||||
* Reine Funktion über typisierten Eingangsdaten, kein React, kein Store, kein
|
||||
* Zugriff auf `DATENSATZ`. Genau diese Grenze macht den Einbau billig: In der
|
||||
* Software ruft der Loader denselben Builder mit echten Daten auf, die
|
||||
* Screen-Komponente bleibt unverändert.
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
*
|
||||
* Wortgleicher Spiegel von `VMState` aus `@hivegrid/view-models` in HiveGrid
|
||||
* Cloud (`packages/view-models/src/index.ts`). Beim Einbau wird dieser Import
|
||||
* gegen das echte Paket getauscht — die Seiten selbst ändern sich dabei nicht.
|
||||
* gegen das echte Paket getauscht, die Seiten selbst ändern sich dabei nicht.
|
||||
*
|
||||
* Warum das hier überhaupt steht: Die Cloud trennt strikt zwischen Route und
|
||||
* Screen. Die Route besitzt Loader, Actions und Realtime; die Screen-Komponente
|
||||
* ist rein und bekommt alles als Prop (`<XPage vm={} actions={} />`). Eine
|
||||
* Komponente, die sich ihre Daten selbst aus einem Store holt, muss beim Einbau
|
||||
* auseinandergenommen werden — genau das soll dieser Prototyp ersparen.
|
||||
* auseinandergenommen werden, genau das soll dieser Prototyp ersparen.
|
||||
*/
|
||||
|
||||
export type VMStatus = "idle" | "loading" | "ready" | "empty" | "error";
|
||||
@@ -26,7 +26,7 @@ export function bereit<T>(data: T): VMState<T> {
|
||||
return { status: "ready", data };
|
||||
}
|
||||
|
||||
/** Kein Datensatz für die aktuelle Auswahl — nicht dasselbe wie ein Fehler. */
|
||||
/** Kein Datensatz für die aktuelle Auswahl, nicht dasselbe wie ein Fehler. */
|
||||
export function leer<T>(): VMState<T> {
|
||||
return { status: "empty" };
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* View-Model der Kundenseite.
|
||||
*
|
||||
* Reine Funktion über typisierten Eingangsdaten — kein React, kein Store, kein
|
||||
* Reine Funktion über typisierten Eingangsdaten, kein React, kein Store, kein
|
||||
* Zugriff auf `DATENSATZ`. Genau diese Grenze macht den Einbau billig: In der
|
||||
* Software ruft der Loader denselben Builder mit echten Daten auf, die
|
||||
* Screen-Komponente bleibt unverändert.
|
||||
@@ -52,7 +52,7 @@ export function baueKundenVM(daten: Datensatz, filter: FilterState): VMState<Kun
|
||||
entwicklung,
|
||||
sparkline: entwicklung.slice(-10).map((p) => p.value),
|
||||
wiederkehrendProzent: wiederkehrquote(daten.kunden),
|
||||
// Bezugsgrösse ist der Kunde mit Historie — Karteileichen würden den
|
||||
// Bezugsgrösse ist der Kunde mit Historie: Karteileichen würden den
|
||||
// Durchschnitt nach unten ziehen.
|
||||
kundenwert: mitAuftrag.length
|
||||
? mitAuftrag.reduce((s, k) => s + k.umsatzGesamt, 0) / mitAuftrag.length
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* View-Model der Teamseite.
|
||||
*
|
||||
* Reine Funktion über typisierten Eingangsdaten — kein React, kein Store, kein
|
||||
* Reine Funktion über typisierten Eingangsdaten, kein React, kein Store, kein
|
||||
* Zugriff auf `DATENSATZ`. Genau diese Grenze macht den Einbau billig: In der
|
||||
* Software ruft der Loader denselben Builder mit echten Daten auf, die
|
||||
* Screen-Komponente bleibt unverändert.
|
||||
@@ -16,7 +16,7 @@ import { bereit, leer, type VMState } from "./index";
|
||||
* Tabellenzeile der Mitarbeiterübersicht.
|
||||
*
|
||||
* Die `id` hängt hier und nicht in der Seite, weil die Tabelle einen stabilen
|
||||
* Schlüssel braucht — das ist eine Eigenschaft des Datensatzes, keine der
|
||||
* Schlüssel braucht, das ist eine Eigenschaft des Datensatzes, keine der
|
||||
* Darstellung.
|
||||
*/
|
||||
export type TeamZeile = MitarbeiterKennzahl & { readonly id: string };
|
||||
@@ -33,7 +33,7 @@ export interface TeamDaten {
|
||||
readonly zeilen: TeamZeile[];
|
||||
}
|
||||
|
||||
/** Anzeigenamen der Abwesenheitsgründe — die Rohwerte sind Kleinschreibung. */
|
||||
/** Anzeigenamen der Abwesenheitsgründe, die Rohwerte sind Kleinschreibung. */
|
||||
const ABWESENHEIT_LABEL: Record<string, string> = {
|
||||
urlaub: "Urlaub",
|
||||
krank: "Krank",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* View-Model der Übersichtsseite.
|
||||
*
|
||||
* Reine Funktion über typisierten Eingangsdaten — kein React, kein Store, kein
|
||||
* Reine Funktion über typisierten Eingangsdaten, kein React, kein Store, kein
|
||||
* Zugriff auf `DATENSATZ`. Genau diese Grenze macht den Einbau billig: In der
|
||||
* Software ruft der Loader denselben Builder mit echten Daten auf, die
|
||||
* Screen-Komponente bleibt unverändert.
|
||||
|
||||
@@ -48,7 +48,7 @@ if (!palette) {
|
||||
} else {
|
||||
palette.forEach((farbe, i) => {
|
||||
if (farbe !== soll[i]) {
|
||||
fehler.push(`Palette Position ${i + 1}: ${farbe} — Carbon: ${soll[i]}`);
|
||||
fehler.push(`Palette Position ${i + 1}: ${farbe}: Carbon: ${soll[i]}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -68,7 +68,7 @@ for (const [feld, refName] of Object.entries(neutraleZuordnung)) {
|
||||
if (ist === null) fehler.push(`CHART.${feld} nicht gefunden`);
|
||||
else if (ist !== sollWert) {
|
||||
fehler.push(
|
||||
`CHART.${feld} = ${ist} — Carbon ${referenz.neutraleWerte[refName].token}: ${sollWert}`
|
||||
`CHART.${feld} = ${ist}: Carbon ${referenz.neutraleWerte[refName].token}: ${sollWert}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -79,11 +79,11 @@ const tooltipBg = hexFeld("CHART", "tooltipBg");
|
||||
const tooltipFg = hexFeld("CHART", "tooltipFg");
|
||||
if (tooltipBg !== referenz.tooltip.hintergrund.wert.toLowerCase()) {
|
||||
fehler.push(
|
||||
`CHART.tooltipBg = ${tooltipBg} — Carbon ${referenz.tooltip.hintergrund.token}: ${referenz.tooltip.hintergrund.wert}`
|
||||
`CHART.tooltipBg = ${tooltipBg}: Carbon ${referenz.tooltip.hintergrund.token}: ${referenz.tooltip.hintergrund.wert}`
|
||||
);
|
||||
}
|
||||
if (tooltipFg !== referenz.tooltip.text.wert.toLowerCase()) {
|
||||
fehler.push(`CHART.tooltipFg = ${tooltipFg} — Carbon: ${referenz.tooltip.text.wert}`);
|
||||
fehler.push(`CHART.tooltipFg = ${tooltipFg}: Carbon: ${referenz.tooltip.text.wert}`);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------- Achsen-Tickanzahl */
|
||||
@@ -91,7 +91,7 @@ if (tooltipFg !== referenz.tooltip.text.wert.toLowerCase()) {
|
||||
const raster = theme.match(/nettesRaster\(\s*\n?\s*max: number,\s*\n?\s*maxIntervalle = (\d+)/);
|
||||
if (raster && Number(raster[1]) !== referenz.achsen.ticksYAchse) {
|
||||
fehler.push(
|
||||
`nettesRaster maxIntervalle = ${raster[1]} — Carbon numberOfTicks (y): ${referenz.achsen.ticksYAchse}`
|
||||
`nettesRaster maxIntervalle = ${raster[1]}: Carbon numberOfTicks (y): ${referenz.achsen.ticksYAchse}`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*
|
||||
* Warum: Textkollisionen treten nur in bestimmten Fensterbreiten auf. Ein
|
||||
* Screenshot bei 1600 px zeigt sie nicht, bei 1280 px schon. Auge und
|
||||
* Einzelscreenshot reichen dafür nicht — das muss gemessen werden.
|
||||
* Einzelscreenshot reichen dafür nicht, das muss gemessen werden.
|
||||
*
|
||||
* Gemessen wird auf Textzeilen (Range.getClientRects), nicht auf Elementkästen:
|
||||
* Sonst melden benachbarte Tabellenzellen mit Innenabstand falsch positiv.
|
||||
@@ -49,7 +49,7 @@ const messen = (MIN) => {
|
||||
if (st.visibility === "hidden" || st.opacity === "0") continue;
|
||||
// Absichtlich bündige Flächen (Segment-Control) ausnehmen
|
||||
if (el.closest("[data-buendig]")) continue;
|
||||
// Nächster Blockcontainer — Wörter innerhalb eines Satzes sind keine Kollision
|
||||
// Nächster Blockcontainer: Wörter innerhalb eines Satzes sind keine Kollision
|
||||
let block = el;
|
||||
while (block && block !== document.body && getComputedStyle(block).display.startsWith("inline")) {
|
||||
block = block.parentElement;
|
||||
@@ -92,7 +92,7 @@ const messen = (MIN) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Die Seite selbst darf nie seitlich scrollen — ein waagerechter
|
||||
// Die Seite selbst darf nie seitlich scrollen, ein waagerechter
|
||||
// Scrollbalken auf Dokumentebene ist immer ein Fehler, kein Kompromiss.
|
||||
const doc = document.documentElement;
|
||||
const seitenUeberlauf = doc.scrollWidth > doc.clientWidth + 1 ? doc.scrollWidth - doc.clientWidth : 0;
|
||||
@@ -119,7 +119,7 @@ for (const breite of BREITEN) {
|
||||
for (const pfad of SEITEN) {
|
||||
const antwort = await seite.goto(BASIS + pfad, { waitUntil: "networkidle" });
|
||||
if (!antwort || antwort.status() >= 400) {
|
||||
console.error(`✗ ${pfad} antwortet mit ${antwort?.status() ?? "keiner Antwort"} — Route existiert nicht.`);
|
||||
console.error(`✗ ${pfad} antwortet mit ${antwort?.status() ?? "keiner Antwort"}: Route existiert nicht.`);
|
||||
process.exit(1);
|
||||
}
|
||||
await seite.waitForTimeout(500);
|
||||
@@ -170,4 +170,4 @@ if (liste.length) {
|
||||
console.error(`\n✗ ${liste.length} Layoutfehler. Mindestabstand ist ${MIN_ABSTAND} px (Carbon $spacing-03).`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`✓ Kein kollidierender Text, kein Überlauf — ${SEITEN.length} Seiten × ${BREITEN.length} Breiten (${BREITEN[0]}–${BREITEN.at(-1)} px).`);
|
||||
console.log(`✓ Kein kollidierender Text, kein Überlauf: ${SEITEN.length} Seiten × ${BREITEN.length} Breiten (${BREITEN[0]}–${BREITEN.at(-1)} px).`);
|
||||
Reference in new issue
Block a user