Files
Dashboard/features/dashboard/pages/KomponentenSeite.tsx
T
dejanandClaude Opus 5 b8bf987d0e feat(dashboard): Analytics-Dashboard als Gestaltungsvorlage zur Übergabe
UI/UX-Prototyp der Analytics-Dashboards in HiveGrid, fachlich auf einen
SHK-/Handwerksbetrieb zugeschnitten. Ziel ist die Übergabe an die Entwicklung,
nicht der Produktivbetrieb.

Inhalt: sechs Seiten (Übersicht, Finanzen, Aufträge, Kunden, Team, Komponenten),
26 KPIs, neun Chart-Bausteine, Filtersystem mit seitenabhängiger Relevanz und ein
deterministischer Demo-Datensatz (420 Kunden, 1.900 Aufträge, Stichtag 30.07.2026).

Drei Quellen sind zusammengeführt: die fachliche Planung mit dem Branchenpartner
(Januar 2026), die Subframe-Komponentenbibliothek aus dem HiveGrid-Pitch und
IBM Carbon Data Visualization für die Diagrammregeln. Herkunft und bewusste
Abweichungen stehen in docs/HERKUNFT.md.

Dokumentation: README, HANDOVER (was echte Logik braucht), docs/DESIGN-SYSTEM,
KPI-KATALOG, DATENMODELL, NAVIGATION, HERKUNFT, ZIELSYSTEM (Einbauort in
HiveGrid Cloud) und LEITPLANKEN (welche Cloud-Screens als Vorbild taugen).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPeZNwSeU7qG3RRCwaxEZX
2026-08-29 17:25:20 +02:00

353 lines
14 KiB
TypeScript

"use client";
import React from "react";
import { PageHeader } from "../components/layout/PageHeader";
import { PageContainer } from "../components/layout/PageContainer";
import { KPICard } from "../components/cards/KPICard";
import { ChartCard } from "../components/cards/ChartCard";
import { AlertList } from "../components/cards/AlertList";
import { StatZeile } from "../components/cards/StatZeile";
import { AreaVerlauf } from "../components/charts/AreaVerlauf";
import { BalkenVertikal } from "../components/charts/BalkenVertikal";
import { BalkenHorizontal } from "../components/charts/BalkenHorizontal";
import { Donut } from "../components/charts/Donut";
import { Trichter } from "../components/charts/Trichter";
import { AuslastungsBalken } from "../components/charts/AuslastungsBalken";
import { ChartLeer } from "../components/charts/ChartLeer";
import { ChartLegende } from "../components/charts/ChartLegende";
import { AuftragBadge, RechnungBadge } from "../components/tables/StatusBadge";
import {
KATEGORIE_FARBEN,
MAX_KATEGORIEN,
PRIMAER_FARBE,
STATUS_FARBEN,
VERGLEICH_FARBE,
} from "../utils/chartTheme";
import {
formatEuro,
formatEuroAchse,
formatEuroKompakt,
formatProzent,
formatZahl,
} from "../utils/formatters";
import { DATENSATZ_INFO } from "../data/seed";
/** Beispieldaten, damit die Galerie unabhängig von Filtern immer etwas zeigt. */
const BEISPIEL_REIHE = Array.from({ length: 12 }, (_, i) => ({
datum: `2026-${String(i + 1).padStart(2, "0")}`,
label: ["Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"][i],
value: [148, 162, 171, 158, 166, 149, 152, 161, 189, 205, 198, 176][i] * 1000,
vergleich: [131, 140, 152, 146, 151, 138, 141, 149, 168, 181, 176, 158][i] * 1000,
}));
const BEISPIEL_KATEGORIEN = [
{ name: "Heizung", value: 842_000 },
{ name: "Sanitär", value: 486_000 },
{ name: "Photovoltaik", value: 421_000 },
{ name: "Klima & Lüftung", value: 268_000 },
{ name: "Elektro", value: 194_000 },
{ name: "Sonstiges", value: 61_000 },
];
export function KomponentenSeite() {
return (
<>
<PageHeader
titel="Komponenten & Regeln"
beschreibung="Alle Bausteine dieses Prototyps an einem Ort — als Referenz für den Einbau in die Software."
/>
<PageContainer>
<Abschnitt
titel="Warum diese Seite existiert"
text="Der Prototyp ist eine Gestaltungsvorlage, kein Produkt. Diese Seite zeigt jeden Baustein isoliert, damit beim Einbau klar ist, welche Komponente wofür gedacht ist und welche Regeln dahinterstehen. Datei-Pfade stehen jeweils darunter."
/>
<ChartCard
id="komp-farben"
titel="Farbpalette"
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">
<div className="flex flex-wrap gap-2">
{KATEGORIE_FARBEN.map((farbe, index) => (
<div key={farbe} className="flex w-[104px] flex-col gap-1">
<span className="h-10 w-full rounded-md" style={{ background: farbe }} />
<span className="text-[11px] font-medium text-text-neutral-secondary">
{index + 1}. {farbe}
</span>
</div>
))}
</div>
<div className="flex flex-wrap gap-4 border-t border-solid border-border-default pt-3">
{Object.entries(STATUS_FARBEN).map(([name, farbe]) => (
<span key={name} className="flex items-center gap-2 text-[12px] text-text-neutral-secondary">
<span className="h-4 w-4 rounded" style={{ background: farbe }} />
Status „{name}“ · {farbe}
</span>
))}
</div>
<p className="text-[12px] leading-relaxed text-text-neutral-tertiary">
Kategorien nutzen die Carbon-Palette, Statusaussagen die Ampelfarben. Beides nie
mischen: eine grüne Kategorie neben einem grünen Positiv-Status ist nicht mehr lesbar.
Maximal {MAX_KATEGORIEN} Segmente je Chart, der Rest wird zu „Sonstige“
zusammengefasst.
</p>
</div>
</ChartCard>
<Abschnitt titel="Kennzahlen-Karten" text="features/dashboard/components/cards/KPICard.tsx" />
<div className="grid grid-cols-2 gap-4 lg:grid-cols-4">
<KPICard
label="Mit Trend und Sparkline"
wert={formatEuro(238_412)}
zusatz="Zahlungseingang im Zeitraum"
trend={{ wert: 12.4, richtung: "up" }}
icon="Euro"
akzent="positiv"
verlauf={[42, 48, 45, 52, 58, 55, 63, 71]}
/>
<KPICard
label="Negativer Trend"
wert={formatProzent(58.2, 0)}
zusatz="Abschlussquote"
trend={{ wert: 6.1, richtung: "down" }}
icon="Target"
akzent="negativ"
/>
<KPICard
label="Kleiner ist besser"
wert="34,2 Tage"
zusatz="Ø Zahlungsdauer"
trend={{ wert: 8.3, richtung: "down" }}
icon="Clock"
akzent="positiv"
richtungGut="negativ"
/>
<KPICard
label="Ohne Trend"
wert={formatZahl(1900)}
zusatz="Vorgänge im Datensatz"
icon="ClipboardList"
akzent="neutral"
/>
</div>
<Hinweis text="richtungGut='negativ' dreht die Farblogik um: Bei Zahlungsdauer oder Reaktionszeit ist ein Rückgang gut. Ohne diesen Schalter würde die Karte eine Verbesserung rot einfärben." />
<div className="grid grid-cols-1 gap-4 xl:grid-cols-3">
<ChartCard
id="komp-area"
titel="Flächendiagramm mit Vergleich"
untertitel="components/charts/AreaVerlauf.tsx — für Zeitverläufe"
className="xl:col-span-2"
legende={
<ChartLegende
eintraege={[
{ name: "Aktuell", farbe: PRIMAER_FARBE },
{ name: "Vorjahr", farbe: VERGLEICH_FARBE, gestrichelt: true },
]}
/>
}
>
<AreaVerlauf
daten={BEISPIEL_REIHE}
formatWert={formatEuroKompakt}
formatAchse={formatEuroAchse}
zeigeVergleich
hoehe={220}
/>
</ChartCard>
<ChartCard
id="komp-donut"
titel="Ringdiagramm"
untertitel="components/charts/Donut.tsx — für Anteile"
>
<Donut daten={BEISPIEL_KATEGORIEN} formatWert={formatEuroKompakt} hoehe={200} />
</ChartCard>
</div>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
<ChartCard
id="komp-balken"
titel="Säulendiagramm"
untertitel="components/charts/BalkenVertikal.tsx — Mengen mit Nulllinie"
>
<BalkenVertikal
daten={BEISPIEL_REIHE.map((p) => ({ label: p.label, value: p.value / 1000 }))}
formatWert={(w) => formatZahl(w)}
hoehe={200}
nurGanzzahlen
/>
</ChartCard>
<ChartCard
id="komp-hbalken"
titel="Balkendiagramm (liegend)"
untertitel="components/charts/BalkenHorizontal.tsx — Rangfolgen"
>
<BalkenHorizontal
daten={BEISPIEL_KATEGORIEN}
formatWert={formatEuroKompakt}
einfarbig={false}
/>
</ChartCard>
<ChartCard
id="komp-trichter"
titel="Trichter"
untertitel="components/charts/Trichter.tsx — Stufen mit Übergangsquote"
>
<Trichter
stufen={[
{ name: "Anfrage", value: 1900 },
{ name: "Vor-Aufmaß", value: 1420 },
{ name: "Angebot", value: 1180 },
{ name: "Beauftragt", value: 690 },
{ name: "Abgeschlossen", value: 612 },
]}
formatWert={(w) => formatZahl(w)}
/>
</ChartCard>
</div>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
<ChartCard
id="komp-auslastung"
titel="Auslastungsbalken"
untertitel="components/charts/AuslastungsBalken.tsx — mit Ampel und 100-%-Marke"
>
<AuslastungsBalken
zeilen={[
{ name: "Thomas Krause", wert: 104, zusatz: "Meister · Heizung" },
{ name: "Dennis Meyer", wert: 92, zusatz: "Monteur · Sanitär" },
{ name: "Maike Schmidt", wert: 78, zusatz: "Monteur · Photovoltaik" },
{ name: "Kevin Wolf", wert: 54, zusatz: "Auszubildender · Elektro" },
]}
/>
</ChartCard>
<ChartCard
id="komp-alerts"
titel="Handlungsbedarf"
untertitel="components/cards/AlertList.tsx — nach Dringlichkeit sortiert"
>
<AlertList
eintraege={[
{
id: "a",
text: "25 überfällige Rechnungen",
detail: "Zahlungsziel überschritten",
betrag: 96_194,
aktion: "Mahnlauf starten",
art: "kritisch",
},
{
id: "b",
text: "32 Aufträge über Termin",
detail: "Ältester seit 99 Tagen fällig",
betrag: 148_600,
aktion: "Termine neu planen",
art: "warnung",
},
{
id: "c",
text: "23 Förderanträge in Bearbeitung",
detail: "Bescheid ausstehend",
betrag: 116_100,
aktion: "Status prüfen",
art: "hinweis",
},
]}
/>
</ChartCard>
<ChartCard
id="komp-leer"
titel="Leerzustand"
untertitel="components/charts/ChartLeer.tsx — greift, wenn Filter alles wegschneiden"
>
<ChartLeer hoehe={180} />
</ChartCard>
</div>
<ChartCard
id="komp-badges"
titel="Status-Kennzeichnung"
untertitel="components/tables/StatusBadge.tsx"
>
<div className="flex flex-col gap-4">
<div className="flex flex-wrap items-center gap-2">
<span className="w-24 text-[12px] text-text-neutral-tertiary">Auftrag</span>
<AuftragBadge status="anfrage" />
<AuftragBadge status="aufmass" />
<AuftragBadge status="angebot" />
<AuftragBadge status="beauftragt" />
<AuftragBadge status="in_arbeit" />
<AuftragBadge status="abgeschlossen" />
<AuftragBadge status="verloren" />
</div>
<div className="flex flex-wrap items-center gap-2">
<span className="w-24 text-[12px] text-text-neutral-tertiary">Rechnung</span>
<RechnungBadge status="entwurf" />
<RechnungBadge status="gestellt" />
<RechnungBadge status="bezahlt" />
<RechnungBadge status="ueberfaellig" />
</div>
<StatZeile
eintraege={[
{ label: "Aufträge", wert: formatZahl(DATENSATZ_INFO.auftraege), hinweis: "im Demo-Datensatz" },
{ label: "Rechnungen", wert: formatZahl(DATENSATZ_INFO.rechnungen) },
{ label: "Kunden", wert: formatZahl(DATENSATZ_INFO.kunden) },
{ label: "Zeiteinträge", wert: formatZahl(DATENSATZ_INFO.zeiteintraege) },
]}
/>
</div>
</ChartCard>
<ChartCard
id="komp-regeln"
titel="Regeln, die beim Einbau erhalten bleiben sollten"
untertitel="Ausführlich in docs/DESIGN-SYSTEM.md"
>
<ul className="flex flex-col gap-2.5">
{[
"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: dunkle Fläche #393939, weißer Text, kein Doppelpunkt.",
"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.",
"Jede Karte braucht einen Leerzustand. Filter können jedes Chart leer laufen lassen.",
].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" />
{regel}
</li>
))}
</ul>
</ChartCard>
</PageContainer>
</>
);
}
function Abschnitt({ titel, text }: { titel: string; text: string }) {
return (
<div className="flex flex-col gap-1 pt-2">
<h2 className="text-[15px] font-semibold text-text-neutral-primary">{titel}</h2>
<p className="max-w-3xl text-[13px] leading-relaxed text-text-neutral-tertiary">{text}</p>
</div>
);
}
function Hinweis({ text }: { text: string }) {
return (
<p className="rounded-md border border-solid border-border-default bg-white px-3 py-2 text-[12px] leading-relaxed text-text-neutral-secondary">
{text}
</p>
);
}