diff --git a/HANDOVER.md b/HANDOVER.md
index 657df1d..35f8753 100644
--- a/HANDOVER.md
+++ b/HANDOVER.md
@@ -10,9 +10,11 @@
> [`docs/ZIELSYSTEM.md`](docs/ZIELSYSTEM.md) Abschnitt 0.
>
> **Ebenfalls vor dem Einbau lesen:**
-> [`docs/SCOPE-ANALYTICS-DASHBOARD.md`](docs/SCOPE-ANALYTICS-DASHBOARD.md).
-> Der Zuschnitt zwischen Analytics und Dashboard ist noch offen und betrifft die
-> Übersichtsseite und den Handlungsbedarf.
+> [`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
+> Entfernen eine offene Produktfrage mitentschieden hätte. Das ist vor dem
+> Port aufzulösen und nicht zu übernehmen.
Dieser Prototyp liefert **Gestaltung und Interaktion**, nicht die Anbindung.
Was echt ist, was Attrappe ist und was beim Einbau zu tun bleibt:
diff --git a/README.md b/README.md
index d3157cf..26ec307 100644
--- a/README.md
+++ b/README.md
@@ -4,16 +4,19 @@ 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.
-> **Offener Scope-Punkt:** „Analytics" und „Dashboard" sind zwei Features, nicht
-> eines. Das Dashboard ist der Begrüßungsbildschirm und zeigt die wichtigsten
-> Charts aus Analytics **plus** anderes (HR-Anträge, Aufgaben, Termine). Was das
-> für den Zuschnitt dieses Prototyps bedeutet, steht in
-> [`docs/SCOPE-ANALYTICS-DASHBOARD.md`](docs/SCOPE-ANALYTICS-DASHBOARD.md) —
-> festgehalten, noch nicht entschieden, noch nichts danach gebaut.
+> **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
+> 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
+> wird, deren Inhalt sich mit dem Dashboard überschneidet. Stand und offene
+> Punkte in
+> [`docs/SCOPE-ANALYTICS-DASHBOARD.md`](docs/SCOPE-ANALYTICS-DASHBOARD.md).
```bash
npm install
-npm run dev # http://localhost:4300 → leitet auf /analytics
+npm run dev # http://localhost:4300 → leitet auf /dashboard
```
---
@@ -46,12 +49,14 @@ features/dashboard/
components/
layout/ AppShell, PageHeader, PageContainer, Navigation
cards/ KPICard, ChartCard, AlertList, StatZeile
+ dashboard/ Kachelraster, Kachelrahmen, Begrüßung (nur /dashboard)
charts/ alle Visualisierungen + gemeinsamer Tooltip
filters/ Filterleiste und Mehrfachauswahl
tables/ DataTable (sortieren, suchen, blättern) + Badges
context/ Filter-, Widget- und Toast-State
data/ types · seed (Dummy-Daten) · calculations · filtering
- hooks/ useAnalyseDaten
+ · nutzer (Attrappe des angemeldeten Benutzers)
+ hooks/ useAnalyseDaten, useIstSchmal
pages/ die sechs Seiten — reine Komponenten ({ vm })
utils/ chartTheme (Carbon-Tokens), formatters, dateUtils
ui/ Subframe-Komponentenbibliothek (unverändert übernommen)
diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx
index 117726b..6ed89e0 100644
--- a/app/dashboard/page.tsx
+++ b/app/dashboard/page.tsx
@@ -1,5 +1,5 @@
-import { DashboardPlatzhalterSeite } from "@/features/dashboard/pages/DashboardPlatzhalterSeite";
+import { DashboardRoute } from "@/features/dashboard/routes/DashboardRoute";
export default function Page() {
- return ;
+ return ;
}
diff --git a/app/page.tsx b/app/page.tsx
index 05e7de5..dd8bc1f 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,11 +1,15 @@
import { redirect } from "next/navigation";
-import { ANALYTICS_START } from "@/features/dashboard/components/layout/navigation";
+import { DASHBOARD_START } from "@/features/dashboard/components/layout/navigation";
/**
- * Der Prototyp bildet das Analytics-Feature ab, nicht das Dashboard.
- * Die Wurzel leitet deshalb auf den Analytics-Einstieg weiter — in der
- * Software liegt unter "/" das Dashboard, das hier nicht enthalten ist.
+ * Die Wurzel leitet auf das Dashboard weiter — so wie in der Software, wo
+ * nach der Anmeldung der Begrüßungsbildschirm steht und nicht der
+ * Analysebereich.
+ *
+ * Bis zum 31.08.2026 zeigte sie auf Analytics, weil es das Dashboard noch
+ * nicht gab. Jetzt gibt es beides, und die Reihenfolge folgt wieder dem
+ * Zielsystem.
*/
export default function Page() {
- redirect(ANALYTICS_START);
+ redirect(DASHBOARD_START);
}
diff --git a/docs/NAVIGATION.md b/docs/NAVIGATION.md
index 9f20ae7..d77bf23 100644
--- a/docs/NAVIGATION.md
+++ b/docs/NAVIGATION.md
@@ -9,7 +9,7 @@ was dazukommt, nicht was schon da ist.
```
HIVEGRID
-├── Dashboard Platzhalterseite — eigenes Feature, hier nicht gestaltet
+├── Dashboard Begrüßungsbildschirm — Widget-Fläche /dashboard
└── Analytics ◄── der Prototyp lebt hier /analytics
├─────────────────────
└── Komponenten Bausteingalerie des Prototyps /komponenten
@@ -69,10 +69,11 @@ Leads mit seinen acht. Drei Gründe geben den Ausschlag:
Ein weiterer Vorteil: Die Leiste wächst. Käme eine sechste Analyse-Seite dazu,
ist das eine Zeile mehr — ein Reiterband würde eng.
-„Dashboard" führt auf eine **Platzhalterseite** unter `/dashboard`: ein
-Leerzustand, der erklärt, warum hier nichts steht und was später hinkommt. Ein
-Menüpunkt, der nur einen Hinweis auslöst, lässt offen, ob etwas fehlt oder
-etwas kaputt ist. Siehe
+„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
+Bereich, das Dashboard eine Seite. Siehe
[SCOPE-ANALYTICS-DASHBOARD.md](SCOPE-ANALYTICS-DASHBOARD.md).
### Schmale Fenster
diff --git a/docs/SCOPE-ANALYTICS-DASHBOARD.md b/docs/SCOPE-ANALYTICS-DASHBOARD.md
index 950f36b..cfcb16e 100644
--- a/docs/SCOPE-ANALYTICS-DASHBOARD.md
+++ b/docs/SCOPE-ANALYTICS-DASHBOARD.md
@@ -1,8 +1,9 @@
# Scope: Analytics und Dashboard sind zwei Features
-**Stand: 29.08.2026.** Von den offenen Punkten in Abschnitt 5 ist bisher genau
-einer entschieden und gebaut: die Navigation (5.5). Alles andere ist mit den
-Kollegen zu klären, bevor jemand Code anfasst.
+**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
+gerade absichtlich im Repo steht** und in Abschnitt 3a benannt ist.
---
@@ -92,6 +93,76 @@ wie sie sind.
---
+## 3a. Was am 31.08.2026 gebaut wurde
+
+Das Dashboard existiert unter `/dashboard` — als **erster Aufschlag mit
+Analytics-Inhalten**, aber als Widget-Fläche gebaut, nicht als Analytics-Seite
+mit anderem Titel.
+
+### Aufbau
+
+| Reihe | Kacheln | Herkunft |
+|---|---|---|
+| Begrüßung | Anrede, Datum, Rolle, ein Satz zur Lage | neu (`hero`-Widget) |
+| Kennzahlen | Umsatz · Offene Forderungen · Aktive Aufträge · Ø Auslastung | Analytics, je ein Link in die Analyse |
+| Handlungsbedarf | die Liste über volle Breite | Analytics-Übersicht |
+| Diagramme | Zahlungseingang · Umsatz nach Gewerk · Vertriebstrichter · Auslastung | Analytics |
+| Kommt noch | Meine Aufgaben · Termine heute · HR-Anträge | reservierte Plätze, **keine Daten** |
+
+### Drei Entscheidungen, die dabei getroffen wurden
+
+**Kein seitenweiter Filter.** Jede Kachel trägt ihren eigenen Zeitraum und
+schreibt ihn in den Kopf („letzte 30 Tage", „zum Stichtag"). Das ist das
+Bedienmodell der Software (5.4) und war die unbequemere Variante: Eine
+Filterleiste über der Seite wäre schneller gebaut gewesen und hätte beim
+Einbau alle Kacheln wieder auseinandergenommen.
+
+**Das Raster ist aus der Software übernommen, nicht erfunden.** Es ist
+zeichengleich das Raster aus `DashboardPage.tsx`; jede Kachel steht in
+``, und diese beiden Zahlen sind dort
+`widget.size.w` und `widget.position.x`. Der Unterschied ist die Herkunft: Im
+Prototyp sind sie geschrieben, in der Software gespeichert und vom Benutzer
+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
+festen Anfang. Vermerkt in `components/dashboard/WidgetRaster.tsx`.
+
+**Die reservierten Plätze zeigen keine erfundenen Zahlen.** „2 neue Anträge"
+aus dem Nichts liest sich in einer Abnahme wie eine fertige Funktion, und
+irgendwann fragt jemand, warum die Zahl nie hochzählt. Die drei Kacheln stehen
+gestrichelt und leer da und nennen, aus welchem Feature sie ihre Daten
+bekommen werden.
+
+### Die Doppelung, die jetzt im Repo steht
+
+**Der Handlungsbedarf steht ab sofort an zwei Stellen:** auf dem Dashboard und
+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.
+
+**Beim Einbau ist das zu bereinigen, nicht zu übernehmen.** Zwei
+Einstiegsseiten mit derselben Arbeitsliste sind kein Entwurf, sondern ein
+Versehen, das sich als Entwurf tarnt.
+
+### Was das Dashboard noch nicht hat
+
+- **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.
+- **Keinen Bearbeitungsmodus.** Die Software hat ihn (`DashboardEditPanel`,
+ Widget-Katalog, gespeichertes Layout). Der Prototyp bildet ihn nicht nach;
+ er zeigt das Ergebnis, nicht das Werkzeug.
+- **Keine Live-Aktualisierung.** Ebenfalls vorhanden in der Software
+ (`liveUpdatesEnabled`) und hier ohne Gegenstück.
+
+---
+
## 4. Wie die beiden zusammenhängen
```
@@ -122,13 +193,17 @@ 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?**
-Der Katalog braucht eine Auswahl. Nicht jedes Chart taugt als Kachel — der
-Vertriebstrichter etwa lebt von der Übergangsquote und wird klein unlesbar.
+**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?**
-Vermutlich Dashboard. Dann fehlt Analytics die Handlungsebene — bewusst
-akzeptieren oder anders lösen.
+**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
+einziger sichtbar doppelt im Repo steht.
**5.4 Zeitraum-Modell im Dashboard.**
Globaler Filter wie in Analytics oder pro Widget wie in P11? Die Software hat
@@ -151,8 +226,15 @@ Unabhängig davon offen, weil Zeiterfassung im v2-Backlog steht — siehe
## 6. Bis zur Klärung
-Am Code ändert sich nichts. Der Prototyp bleibt in seinem jetzigen Zuschnitt
-brauchbar: Die Diagrammbausteine, Kennzahl-Definitionen und Regeln gelten
-unabhängig davon, wie die beiden Features später geschnitten werden. Die
-Übersichtsseite ist im Zweifel der Entwurf für das Dashboard — dann wäre sie
-nicht falsch, sondern nur am falschen Ort.
+Die Diagrammbausteine, Kennzahl-Definitionen und Regeln gelten unabhängig
+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.
+Fällt sie anders, verliert das Dashboard nichts; die Übersicht behält den
+Handlungsbedarf und das Dashboard gibt ihn ab.
+
+In beiden Fällen ist die Doppelung aus Abschnitt 3a aufzulösen, bevor der Port
+beginnt.
diff --git a/docs/ZIELSYSTEM.md b/docs/ZIELSYSTEM.md
index caee16b..a543c3c 100644
--- a/docs/ZIELSYSTEM.md
+++ b/docs/ZIELSYSTEM.md
@@ -53,13 +53,20 @@ Nur das Fachliche und das Analytics-Eigene:
### Was aus diesem Repo Gerüst ist und wegfällt
`components/layout/AppShell.tsx`, unsere Kopie der Seitenleiste, `PageHeader`,
-die Kopfzeile mit Suche und Glocke, die Routen unter `app/` und die
-Platzhalterseite für das Dashboard. All das existiert, damit der Prototyp
-alleine lauffähig und beurteilbar ist. Beim Einbau übernehmen
-`SubframeAppShell`, `AppSidebar`, `PageShell` und die Loader der Software.
+die Kopfzeile mit Suche und Glocke, die Routen unter `app/` und die Attrappe
+des angemeldeten Benutzers (`data/nutzer.ts`). All das existiert, damit der
+Prototyp alleine lauffähig und beurteilbar ist. Beim Einbau übernehmen
+`SubframeAppShell`, `AppSidebar`, `PageShell`, `requireMember` und die Loader
+der Software.
-**Kurz:** Aus diesem Repo kommt der Inhalt des Analytics-Bereichs und die
-Verabredung, wo er in der Navigation sitzt. Alles andere kommt aus HiveGrid.
+Das **Kachelraster des Dashboards** ist die Ausnahme in dieser Liste: Es ist
+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.
+
+**Kurz:** Aus diesem Repo kommt der Inhalt des Analytics-Bereichs, der Aufbau
+des Dashboards und die Verabredung, wo beide in der Navigation sitzen. Alles
+andere kommt aus HiveGrid.
---
diff --git a/features/dashboard/components/cards/KPICard.tsx b/features/dashboard/components/cards/KPICard.tsx
index 6161098..00f1944 100644
--- a/features/dashboard/components/cards/KPICard.tsx
+++ b/features/dashboard/components/cards/KPICard.tsx
@@ -35,6 +35,12 @@ export function KPICard({
trend,
trendLabel = "Vorperiode",
bezug = "zeitraum",
+ /**
+ * Ü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.
+ */
+ bezugText,
icon,
verlauf,
akzent = "neutral",
@@ -47,6 +53,7 @@ export function KPICard({
trend?: { wert: number; richtung: "up" | "down" | "flat" };
trendLabel?: string;
bezug?: Bezug;
+ bezugText?: string;
icon: string;
verlauf?: number[];
/**
@@ -131,7 +138,7 @@ export function KPICard({
) : (
- {BEZUG_LABEL[bezug]}
+ {bezugText ?? BEZUG_LABEL[bezug]}
)}
diff --git a/features/dashboard/components/charts/AreaVerlauf.tsx b/features/dashboard/components/charts/AreaVerlauf.tsx
index b70a114..6c442c8 100644
--- a/features/dashboard/components/charts/AreaVerlauf.tsx
+++ b/features/dashboard/components/charts/AreaVerlauf.tsx
@@ -66,9 +66,25 @@ export function AreaVerlauf({
const hatWerte = daten.some((d) => d.value > 0 || (d.vergleich ?? 0) > 0);
if (!daten.length || !hatWerte) return ;
- // Bei vielen Punkten nur jede n-te Beschriftung zeigen, sonst überlappen sie.
- const intervall =
- daten.length > MAX_ACHSENLABELS ? Math.ceil(daten.length / MAX_ACHSENLABELS) - 1 : 0;
+ /**
+ * Beschriftungen der Zeitachse ausdünnen — zweistufig, weil zwei
+ * verschiedene Dinge sie zum Kollidieren bringen.
+ *
+ * Viele Punkte: Bei mehr als zwölf Werten wird fest jede n-te Beschriftung
+ * gezeigt. Das ist eine Entscheidung über die Lesbarkeit der Reihe und darf
+ * nicht von der Kartenbreite abhängen, sonst zeigt dieselbe Reihe auf zwei
+ * Bildschirmen verschiedene Stützstellen.
+ *
+ * Wenige Punkte in einer schmalen Karte: Fünf Kalenderwochen brauchen rund
+ * 200 px. Eine halbe Kachel bei 768 px hat 150 px — dort liefen "KW 27" und
+ * "KW 28" ineinander. `preserveStartEnd` mit `minTickGap` überlässt Recharts
+ * das Weglassen und hält dabei die erste und letzte Beschriftung, damit der
+ * Zeitraum ablesbar bleibt.
+ */
+ const vieleWerte = daten.length > MAX_ACHSENLABELS;
+ const intervall = vieleWerte
+ ? Math.ceil(daten.length / MAX_ACHSENLABELS) - 1
+ : ("preserveStartEnd" as const);
const maximum = Math.max(
...daten.map((d) => Math.max(d.value, zeigeVergleich ? (d.vergleich ?? 0) : 0))
@@ -87,7 +103,7 @@ export function AreaVerlauf({
-
+
+
+
+ {datum} · {rolle}
+
+
+ {anrede}, {name}
+
+
+
+
+ {kritisch ? (
+
+ ) : (
+
+ )}
+ {lage}
+
+
+ );
+}
diff --git a/features/dashboard/components/dashboard/ReservierteKachel.tsx b/features/dashboard/components/dashboard/ReservierteKachel.tsx
new file mode 100644
index 0000000..630fd38
--- /dev/null
+++ b/features/dashboard/components/dashboard/ReservierteKachel.tsx
@@ -0,0 +1,43 @@
+import React from "react";
+import { FeatherIcon } from "../layout/FeatherIcon";
+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
+ * P11 und haben in der Software bereits ihre Widget-Domäne (`tasks`,
+ * `schedule`, `hr`). Diese Kacheln halten ihnen den Platz frei.
+ *
+ * Sie zeigen ausdrücklich **keine erfundenen Zahlen**. Eine Kachel mit
+ * „2 neue Anträge" aus dem Nichts liest sich in einer Abnahme wie eine
+ * fertige Funktion, und irgendwann fragt jemand, warum die Zahl nie
+ * hochzählt. Der leere Platz sagt dasselbe ehrlicher: Hier kommt etwas hin,
+ * die Fläche ist dafür vorgesehen, das Raster muss dafür nicht umgebaut
+ * werden.
+ *
+ * Gestrichelte Kante statt durchgezogener: Das ist die Konvention für
+ * „noch nicht da" und unterscheidet die Kachel auf einen Blick von den
+ * echten daneben, ohne sie zu verstecken.
+ */
+export function ReservierteKachel({ bereich }: { bereich: ReservierterBereich }) {
+ return (
+
+
+
+
+
+
+ {bereich.titel}
+
+
+
+
{bereich.beschreibung}
+
+
+ Folgt mit {"„"}{bereich.quelle}{"“"}
+
+
+ );
+}
diff --git a/features/dashboard/components/dashboard/WidgetKachel.tsx b/features/dashboard/components/dashboard/WidgetKachel.tsx
new file mode 100644
index 0000000..391afa6
--- /dev/null
+++ b/features/dashboard/components/dashboard/WidgetKachel.tsx
@@ -0,0 +1,127 @@
+"use client";
+
+import React from "react";
+import Link from "next/link";
+import { FeatherArrowRight, FeatherInfo } from "@subframe/core";
+import { FeatherIcon } from "../layout/FeatherIcon";
+import { ZEITRAUM_LABEL, type WidgetDomaene, type WidgetZeitraum } from "../../view-models/dashboard.vm";
+
+/**
+ * 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
+ * Ausblenden gehören zu Analytics; im Dashboard macht das die Widget-Registry
+ * der Software (`docs/SCOPE-ANALYTICS-DASHBOARD.md`, Abschnitt 3.3).
+ *
+ * Zwei Dinge hat die Kachel dafür, die eine Analytics-Karte nicht braucht:
+ *
+ * 1. **Ein Domänen-Icon oben rechts.** Die Software führt dafür eine feste
+ * Zuordnung (`domainIcons` in `DashboardPage.tsx`); sie ist unten
+ * übernommen. Auf einer Fläche, die Zahlen aus vier Bereichen mischt, sagt
+ * das Icon in einem Blick, woher eine Kachel kommt.
+ *
+ * 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
+ * zeigt eine Kachel stillschweigend einen anderen Zeitraum als ihre
+ * Nachbarin, und niemand merkt es.
+ */
+
+/** Zeichengleich zu `domainIcons` in `DashboardPage.tsx` der Software. */
+const DOMAENEN_ICON: Record = {
+ general: "Home",
+ sales: "TrendingUp",
+ finance: "CreditCard",
+ projects: "Folder",
+ tasks: "CheckSquare",
+ hr: "Users",
+ activity: "Activity",
+ comms: "PhoneCall",
+ schedule: "Calendar",
+};
+
+export function WidgetKachel({
+ titel,
+ untertitel,
+ hinweis,
+ domaene,
+ zeitraum,
+ absprung,
+ absprungText = "Details ansehen",
+ children,
+ className = "",
+}: {
+ titel: string;
+ untertitel?: string;
+ /** 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. */
+ absprung?: string;
+ absprungText?: string;
+ children: React.ReactNode;
+ className?: string;
+}) {
+ return (
+
+
+
+
+
+ {titel}
+
+ {hinweis ? (
+
+
+
+ ) : null}
+
+ {untertitel ? (
+
+ {untertitel}
+
+ ) : null}
+
+
+
+ {/* Der Zeitraum steht nur dort, wo Platz für Wörter ist: ab 24rem
+ Kachelbreite. Darunter nimmt er dem Untertitel so viel Raum, dass
+ der auf 77 px Breite zerfällt und in das Diagramm darunter läuft.
+ Er ist dann über den Untertitel weiterhin erschließbar, der die
+ Periode ausschreibt. */}
+
+ {ZEITRAUM_LABEL[zeitraum]}
+
+
+
+
+
+
+
+
{children}
+
+ {absprung ? (
+
+ ) : null}
+
+ );
+}
diff --git a/features/dashboard/components/dashboard/WidgetRaster.tsx b/features/dashboard/components/dashboard/WidgetRaster.tsx
new file mode 100644
index 0000000..de9a099
--- /dev/null
+++ b/features/dashboard/components/dashboard/WidgetRaster.tsx
@@ -0,0 +1,97 @@
+import React from "react";
+
+/**
+ * Das Kachelraster des Dashboards.
+ *
+ * Übernommen aus `DashboardPage.tsx` der Software — dort steht wörtlich
+ *
+ *
+ *
+ * und jede Kachel bekommt ihre Spaltenzahl aus `widget.size.w` und ihren
+ * Anfang aus `widget.position.x`. Genau dieses Raster ist hier nachgebaut,
+ * damit die Kacheln beim Einbau nicht umgerechnet werden müssen: Aus
+ * `` wird dort ein Datensatz
+ * `{ 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
+ * Benutzer das Layout bearbeiten.
+ */
+export function WidgetRaster({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * Eine Rasterzelle. Zeichengleich zu `dashboardWidgetGridClass` in der
+ * 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
+ * Kachel dank `grid-flow-row-dense` trotzdem an die richtige Stelle, nur
+ * ohne festen Anfang.
+ */
+export function Widget({
+ breite,
+ start,
+ mittelVoll = false,
+ children,
+}: {
+ /** Spalten von zwölf — entspricht `widget.size.w`. */
+ breite: 3 | 4 | 6 | 8 | 12;
+ /** 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.
+ */
+ mittelVoll?: boolean;
+ children: React.ReactNode;
+}) {
+ const breiteKlasse =
+ breite <= 3
+ ? "lg:col-span-3"
+ : breite <= 4
+ ? "lg:col-span-4"
+ : breite <= 6
+ ? "lg:col-span-6"
+ : breite <= 8
+ ? "lg:col-span-8"
+ : "lg:col-span-12";
+
+ const startKlasse =
+ start === 0
+ ? "lg:col-start-1"
+ : start === 3
+ ? "lg:col-start-4"
+ : start === 4
+ ? "lg:col-start-5"
+ : start === 6
+ ? "lg:col-start-7"
+ : start === 8
+ ? "lg:col-start-9"
+ : start === 9
+ ? "lg:col-start-10"
+ : "";
+
+ /**
+ * Zwischen `md` und `lg` steht das Raster auf zwei Spalten. Was dort halbiert
+ * 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
+ * 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
+ * volle Breite und teilen sich erst ab `lg` eine Zeile.
+ */
+ const mittel = breite >= 6 || mittelVoll ? "md:col-span-2" : "";
+
+ return
{children}
;
+}
diff --git a/features/dashboard/components/layout/navigation.ts b/features/dashboard/components/layout/navigation.ts
index fe6bb45..0138f72 100644
--- a/features/dashboard/components/layout/navigation.ts
+++ b/features/dashboard/components/layout/navigation.ts
@@ -12,9 +12,9 @@ import type { SeitenId } from "../../data/filtering";
* (`ui/components/SidebarWithSections`, aus der Cloud-Fassung portiert), damit
* Aussehen und Verhalten übereinstimmen.
*
- * „Dashboard" führt auf eine Platzhalterseite: Es ist ein eigenes Feature und
- * in diesem Prototyp nicht gestaltet (siehe
- * `docs/SCOPE-ANALYTICS-DASHBOARD.md`).
+ * „Dashboard" und „Analytics" sind zwei Features mit zwei Zuschnitten: Das
+ * Dashboard ist eine einzelne Seite aus Kacheln, Analytics ein Bereich mit
+ * fünf Seiten und innerer Leiste (siehe `docs/SCOPE-ANALYTICS-DASHBOARD.md`).
*/
export type NavZiel = { art: "route"; href: string };
@@ -34,22 +34,24 @@ export interface NavGruppe {
eintraege: NavEintrag[];
}
-/** Einstieg des Analytics-Bereichs — auch das Ziel der Weiterleitung von „/". */
+/** Einstieg des Analytics-Bereichs. */
export const ANALYTICS_START = "/analytics";
+/** Der Begrüßungsbildschirm — Ziel der Weiterleitung von „/", wie in der Software. */
+export const DASHBOARD_START = "/dashboard";
+
export const NAV_GRUPPEN: NavGruppe[] = [
{
id: "start",
titel: "",
eintraege: [
{
- // Eigenes Feature, in diesem Prototyp nicht gestaltet. Der Punkt führt
- // trotzdem auf eine Seite: Ein Menüeintrag, der nur einen Hinweis
- // auslöst, lässt offen, ob etwas fehlt oder etwas kaputt ist.
+ // Der Begrüßungsbildschirm — eine einzelne Seite, deshalb ohne innere
+ // Leiste. Er ist das Ziel der Weiterleitung von „/".
id: "dashboard",
label: "Dashboard",
icon: "LayoutDashboard",
- ziel: { art: "route", href: "/dashboard" },
+ ziel: { art: "route", href: DASHBOARD_START },
},
{
// Der neue Menüpunkt. Die fünf Analyse-Seiten sind ausschließlich hier
diff --git a/features/dashboard/data/nutzer.ts b/features/dashboard/data/nutzer.ts
new file mode 100644
index 0000000..e898688
--- /dev/null
+++ b/features/dashboard/data/nutzer.ts
@@ -0,0 +1,19 @@
+/**
+ * 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,
+ * damit die Begrüßung etwas zu begrüßen hat.
+ *
+ * Die Rolle steht bewusst dabei: Das Dashboard ist laut Designer-Brief P11
+ * rollenbezogen (GF, Büro, Monteur). Dieser Prototyp gestaltet die
+ * Geschäftsführungs-Sicht; die beiden anderen Layouts entstehen aus denselben
+ * Kacheln in anderer Zusammenstellung.
+ */
+export const NUTZER = {
+ name: "Dejan Cosic",
+ /** Für die Anrede — „Guten Tag, Dejan" liest sich wie eine Ansprache, nicht wie ein Datensatz. */
+ vorname: "Dejan",
+ rolle: "Geschäftsführung",
+ rolleKey: "gf" as const,
+};
diff --git a/features/dashboard/pages/DashboardPlatzhalterSeite.tsx b/features/dashboard/pages/DashboardPlatzhalterSeite.tsx
deleted file mode 100644
index 9e56f98..0000000
--- a/features/dashboard/pages/DashboardPlatzhalterSeite.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-"use client";
-
-import Link from "next/link";
-import { FeatherArrowRight } from "@subframe/core";
-import { Button } from "@/ui/components/Button";
-import { EmptyState } from "@/ui/components/EmptyState";
-import { PageHeader } from "../components/layout/PageHeader";
-import { PageContainer } from "../components/layout/PageContainer";
-import { ANALYTICS_START } from "../components/layout/navigation";
-
-/**
- * Platzhalter für das Dashboard.
- *
- * Das Dashboard ist ein eigenes Feature und in diesem Prototyp nicht
- * gestaltet — siehe `docs/SCOPE-ANALYTICS-DASHBOARD.md`. Der Menüpunkt führt
- * trotzdem auf eine echte Seite statt nur einen Hinweis auszulösen: Ein
- * Navigationspunkt, der nirgendwohin führt, lässt den Leser im Unklaren, ob
- * etwas fehlt oder etwas kaputt ist. Diese Seite sagt es ihm.
- *
- * Der Inhalt beschreibt, was hier später steht — nicht als Gestaltung, sondern
- * als Verabredung. So bleibt die Seite ehrlich leer und trotzdem nützlich.
- */
-export function DashboardPlatzhalterSeite() {
- return (
- <>
-
-
-
-
-
- Analytics und Dashboard sind zwei Features. Dieser Prototyp
- gestaltet Analytics — den Analysebereich mit
- Zeitraum- und Dimensionsfiltern. Das Dashboard beantwortet eine
- andere Frage: {"„Wie ist meine Lage?“ statt „Warum ist das so?“."}
-
-
- Geplant sind drei feste Rollen-Layouts (Geschäftsführung, Büro,
- Monteur) aus Widgets. Ein Teil davon kommt aus Analytics —
- Kennzahlen-Kacheln, Trend-Diagramme, Listen. Der andere Teil
- nicht: offene HR-Anträge, eigene Aufgaben, Termine des Tages.
-
-
- Zuschnitt und offene Punkte stehen in{" "}
-
- docs/SCOPE-ANALYTICS-DASHBOARD.md
-
-
-
- }
- action={
-
- }>
- Zu Analytics
-
-
- }
- />
-
- >
- );
-}
diff --git a/features/dashboard/pages/DashboardSeite.tsx b/features/dashboard/pages/DashboardSeite.tsx
new file mode 100644
index 0000000..d9278b5
--- /dev/null
+++ b/features/dashboard/pages/DashboardSeite.tsx
@@ -0,0 +1,255 @@
+"use client";
+
+import React from "react";
+import Link from "next/link";
+import { Begruessung } from "../components/dashboard/Begruessung";
+import { ReservierteKachel } from "../components/dashboard/ReservierteKachel";
+import { Widget, WidgetRaster } from "../components/dashboard/WidgetRaster";
+import { WidgetKachel } from "../components/dashboard/WidgetKachel";
+import { KPICard } from "../components/cards/KPICard";
+import { AlertList } from "../components/cards/AlertList";
+import { AreaVerlauf } from "../components/charts/AreaVerlauf";
+import { BalkenHorizontal } from "../components/charts/BalkenHorizontal";
+import { AuslastungsBalken } from "../components/charts/AuslastungsBalken";
+import { Trichter } from "../components/charts/Trichter";
+import { ChartLegende } from "../components/charts/ChartLegende";
+import { SeitenZustand } from "../components/layout/SeitenZustand";
+import { PRIMAER_FARBE, VERGLEICH_FARBE } from "../utils/chartTheme";
+import {
+ formatEuro,
+ formatEuroAchse,
+ formatEuroKompakt,
+ formatProzent,
+ formatZahl,
+} from "../utils/formatters";
+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.
+ *
+ * ## Was hier steht und warum
+ *
+ * Das Dashboard beantwortet „Wie ist meine Lage?", Analytics beantwortet
+ * „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
+ * Analyse, die sie erklä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.
+ *
+ * Absteigend nach Dringlichkeit, nicht nach Fachgebiet. Ein Dashboard, das
+ * nach Fachgebieten sortiert ist, ist ein Inhaltsverzeichnis.
+ *
+ * ## Wie das Raster zu lesen ist
+ *
+ * Jede Kachel steht in ``. 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
+ * sie geschrieben. Das ist der einzige Unterschied; das Raster darunter ist
+ * dasselbe (siehe `WidgetRaster.tsx`).
+ *
+ * ## Was hier bewusst fehlt
+ *
+ * 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
+ * Kacheln beim Einbau alle wieder auseinandernehmen.
+ *
+ * Auch keine Rollenumschaltung. Der Prototyp gestaltet die Sicht der
+ * Geschäftsführung; die Layouts für Büro und Monteur entstehen aus denselben
+ * Kacheln in anderer Zusammenstellung und sind eine Produktentscheidung,
+ * keine Gestaltungsfrage.
+ */
+export function DashboardSeite({ vm }: { vm: VMState }) {
+ if (vm.status !== "ready") {
+ return ;
+ }
+ const d = vm.data;
+ const kritisch = d.handlungsbedarf.some((a) => a.art === "kritisch");
+
+ /** Die vier Kennzahlen sitzen nebeneinander in je drei von zwölf Spalten. */
+ const kennzahlStart = [0, 3, 6, 9] as const;
+
+ return (
+ <>
+
+
+
+
+ {d.kennzahlen.map((kachel, index) => (
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* mittelVoll: Der Verlauf daneben nimmt zwischen `md` und `lg` die
+ volle Breite. Ohne das stünde diese Kachel dort allein in einer
+ halben Zeile, und die andere Hälfte bliebe weiß. */}
+
+
+
+
+
+
+
+
+ formatZahl(w)} />
+
+
+
+
+
+
+
+
+
+ {/* Trennung vor den leeren Plätzen: Ohne sie stehen drei Kacheln
+ ohne Inhalt neben sechs mit Inhalt, und der Leser sucht den
+ Ladefehler. Die Überschrift beantwortet die Frage vorher. */}
+
+
+
+ Kommt noch
+
+
+ Das Dashboard zeigt heute nur Auswertungen. Diese drei Bereiche sind dafür
+ vorgesehen und haben in HiveGrid bereits ihre Widget-Domäne — sie brauchen kein
+ neues Raster, nur Daten.
+