Files
Dashboard/features/dashboard/pages/DashboardSeite.tsx
T
dejanandClaude Opus 5 6a0b36ddcc Geviertstriche aus Text, Kommentaren und Doku entfernen
467 Stellen. Nicht durch einen anderen Strich ersetzt, sondern durch das
Satzzeichen, das an der jeweiligen Stelle richtig ist: Doppelpunkt, wo eine
Erlaeuterung folgt; Komma, wo der Satz weiterlaeuft; Punkt, wo zwei
Hauptsaetze aneinanderstiessen. Ein Halbgeviertstrich waere nur derselbe
Griff mit kuerzerem Strich gewesen.

Regelbasiert ersetzt und danach nachgeprueft, weil eine Regel allein nicht
reicht. Gefunden und einzeln korrigiert:

- Eingeschobene Saetze mit Strichpaar. Nur der oeffnende Strich passte ins
  Muster, der schliessende blieb als "—," stehen. Drei Stellen, jetzt mit
  Klammern oder als eigener Satz.

- Doppelte Doppelpunkte, wo beide Striche eines Paars zu Doppelpunkten wurden
  ("Globale Regeln: Schrift, Groessen, Abstaende: kommen aus HiveGrid").
  Sieben Stellen in Doku und Oberflaeche, jetzt in Klammern.

- Pfade, denen ein Komma statt eines Doppelpunkts folgte.

- Ausgerichtete Kommentartabellen, in denen der Strich eine Spalte war.

Nicht angefasst: drei Geviertstriche, die keine Prosa sind, sondern
Tabellenplatzhalter fuer "kein Wert" (Trichter, Auftraege, Kunden). Dort ist
der Strich die Angabe selbst; ihn zu entfernen liesse leere Zellen zurueck.

tsc, lint, build, verify:carbon und verify:layout laufen sauber.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPeZNwSeU7qG3RRCwaxEZX
2026-08-31 20:51: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, gestrichelt: true },
]}
/>
<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>
);
}