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
258 lines
11 KiB
TypeScript
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>
|
|
);
|
|
}
|