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
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, 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>
|
|
);
|
|
}
|