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
353 lines
14 KiB
TypeScript
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>
|
|
);
|
|
}
|