Files
dejanandClaude Opus 5 4ef7c1b27f Legende: gleicher Marker fuer alle Reihen, nur die Farbe unterscheidet
Der Eintrag "Vorjahr" hatte erst ein Kaestchen mit gestricheltem Rand (auf
10 px nur noch Eckstriche), dann ein gestricheltes Linienstueck. Beides war
zu viel Aufwand fuer eine Auskunft, die die Farbe schon gibt.

Jetzt derselbe abgerundete Marker wie beim aktuellen Zeitraum, in Grau. Die
Reihe liest sich ruhiger, weil alle Eintraege dieselbe Form haben.

Die Eigenschaft `gestrichelt` faellt damit weg, in der Legende und an ihren
vier Aufrufstellen. Eine Eigenschaft, die nichts mehr aendert, waere sonst
beim Einbau mitgewandert.

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

258 lines
11 KiB
TypeScript

"use client";
import React from "react";
import Link from "next/link";
import { Begruessung } from "../components/dashboard/Begruessung";
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 `<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
* 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<DashboardDaten> }) {
if (vm.status !== "ready") {
return <SeitenZustand vm={vm} />;
}
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 (
<>
<Begruessung
anrede={d.begruessung.anrede}
name={d.begruessung.name}
datum={d.begruessung.datum}
rolle={d.begruessung.rolle}
lage={d.lage}
kritisch={kritisch}
/>
<div className="flex flex-col gap-6 px-4 pb-10 sm:px-6">
<WidgetRaster>
{d.kennzahlen.map((kachel, index) => (
<Widget key={kachel.key} breite={3} start={kennzahlStart[index]}>
<KennzahlWidget kachel={kachel} sparkline={kachel.key === "umsatz" ? d.sparkline : undefined} />
</Widget>
))}
<Widget breite={12} start={0}>
<WidgetKachel
titel="Handlungsbedarf"
untertitel="Sortiert nach Dringlichkeit, jede Zeile führt in die zugehörige Arbeitsliste"
domaene="general"
zeitraum="stichtag"
>
<AlertList eintraege={d.handlungsbedarf} spalten={2} />
</WidgetKachel>
</Widget>
<Widget breite={8} start={0}>
<WidgetKachel
titel="Zahlungseingang im Zeitverlauf"
untertitel="Aktueller Zeitraum gegen den Vorjahreszeitraum"
hinweis="Gezählt wird der Tag des Zahlungseingangs, nicht das Rechnungsdatum. Dadurch entspricht die Kurve dem tatsächlichen Geldfluss."
domaene="finance"
zeitraum="30_tage"
absprung="/analytics/finanzen"
absprungText="In den Finanzen auswerten"
>
<ChartLegende
eintraege={[
{ name: "Aktueller Zeitraum", farbe: PRIMAER_FARBE },
{ name: "Vorjahr", farbe: VERGLEICH_FARBE },
]}
/>
<AreaVerlauf
daten={d.umsatzVerlauf}
formatWert={formatEuroKompakt}
formatAchse={formatEuroAchse}
zeigeVergleich
hoehe={240}
/>
</WidgetKachel>
</Widget>
{/* 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ß. */}
<Widget breite={4} start={8} mittelVoll>
<WidgetKachel
titel="Umsatz nach Gewerk"
untertitel="Zahlungseingang der letzten 30 Tage"
hinweis="Fünf Gewerke in fester Rangfolge: dieselbe Kodierung wie auf der Finanzseite."
domaene="finance"
zeitraum="30_tage"
absprung="/analytics/finanzen"
>
<BalkenHorizontal
daten={d.gewerkUmsatz}
formatWert={formatEuroKompakt}
einfarbig={false}
/>
</WidgetKachel>
</Widget>
<Widget breite={6} start={0}>
<WidgetKachel
titel="Vertriebstrichter"
untertitel="Angebotsgeschäft ab 6.000 €, zum Stichtag"
hinweis="Nur Vorhaben, für die ein schriftliches Angebot erstellt wird. Wartung und Notdienst laufen ohne Angebot und würden die Quoten verfälschen."
domaene="sales"
zeitraum="stichtag"
absprung="/analytics/auftraege"
absprungText="Aufträge auswerten"
>
<Trichter stufen={d.trichter} formatWert={(w) => formatZahl(w)} />
</WidgetKachel>
</Widget>
<Widget breite={6} start={6}>
<WidgetKachel
titel="Auslastung Monteure"
untertitel="Produktive Stunden gegen verfügbare Stunden"
hinweis="Nur auf Aufträge gebuchte Stunden zählen als produktiv. Über 100 % bedeutet Überstunden."
domaene="hr"
zeitraum="30_tage"
absprung="/analytics/team"
absprungText="Ganzes Team ansehen"
>
<AuslastungsBalken zeilen={d.auslastung} />
</WidgetKachel>
</Widget>
{/* ────────────────────────────────────────────────────────────────
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
neues Raster, nur Daten.
Konkret kommen sie als drei weitere `<Widget breite={4}>` mit
`start={0}`, `{4}` und `{8}` unter diese Zeile. Das Raster darüber
bleibt dabei unangetastet; genau dafür ist es aus
`DashboardPage.tsx` übernommen.
Die Kacheln sind beschrieben in `RESERVIERTE_BEREICHE`
(view-models/dashboard.vm.ts) und zu sehen unter „/komponenten",
Abschnitt „Dashboard-Kacheln". Der Zuschnitt steht in
docs/SCOPE-ANALYTICS-DASHBOARD.md, Abschnitt 3a.
Warum sie hier nicht als Platzhalter stehen: Der
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.
──────────────────────────────────────────────────────────────── */}
</WidgetRaster>
</div>
</>
);
}
/**
* Eine Kennzahl als Kachel.
*
* 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`).
*/
function KennzahlWidget({
kachel,
sparkline,
}: {
kachel: KennzahlKachel;
sparkline?: number[];
}) {
const wert =
kachel.formatAls === "euro"
? formatEuro(kachel.wert)
: kachel.formatAls === "prozent"
? formatProzent(kachel.wert, 0)
: formatZahl(kachel.wert);
return (
<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`}
>
<KPICard
label={kachel.titel}
wert={wert}
zusatz={kachel.zusatz}
trend={kachel.trend}
/* Der Zeitraum der Kachel, ausgeschrieben. "im gewählten Zeitraum"
wie in Analytics wäre hier eine Lüge: Auf dem Dashboard hat niemand
etwas gewählt. */
bezugText={ZEITRAUM_LABEL[kachel.zeitraum]}
icon={kachel.icon}
akzent={kachel.akzent}
richtungGut={kachel.richtungGut}
verlauf={sparkline}
/>
</Link>
);
}