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
162 lines
6.3 KiB
TypeScript
162 lines
6.3 KiB
TypeScript
"use client";
|
|
|
|
import React from "react";
|
|
import { FeatherArrowDownRight, FeatherArrowUpRight, FeatherMinus } from "@subframe/core";
|
|
import { FeatherIcon } from "../layout/FeatherIcon";
|
|
import { Sparkline } from "../charts/Sparkline";
|
|
import { CHART, STATUS_FARBEN } from "../../utils/chartTheme";
|
|
|
|
/** Zeitbezug einer Kennzahl: siehe docs/KPI-KATALOG.md. */
|
|
export type Bezug = "zeitraum" | "stichtag" | "gesamt";
|
|
|
|
const BEZUG_LABEL: Record<Bezug, string> = {
|
|
zeitraum: "im gewählten Zeitraum",
|
|
stichtag: "zum Stichtag",
|
|
gesamt: "über alle Zeiträume",
|
|
};
|
|
|
|
/**
|
|
* KPI-Karte nach UI_UX_GUIDELINES.md, Abschnitt 4:
|
|
* Label oben links · Icon oben rechts · Wert · Zusatz · Fußzeile.
|
|
*
|
|
* Alle Karten einer Reihe haben dieselbe Anatomie, auch die ohne Trend.
|
|
* Wo kein Vorperiodenvergleich möglich ist (stichtagsbezogene Kennzahlen),
|
|
* trägt die Fußzeile den Zeitbezug. Ohne das steht in einer Reihe eine
|
|
* Karte mit Trendzeile neben vier Karten mit leerem Fuß, und das Auge
|
|
* sucht eine Regel, die es nicht gibt.
|
|
*
|
|
* Der Trend vergleicht immer mit der gleich langen Vorperiode
|
|
* (30 Tage gegen die 30 Tage davor): nicht mit dem Vorjahr.
|
|
*/
|
|
export function KPICard({
|
|
label,
|
|
wert,
|
|
zusatz,
|
|
trend,
|
|
trendLabel = "Vorperiode",
|
|
bezug = "zeitraum",
|
|
/**
|
|
* Überschreibt die Fußzeile. Analytics hat einen seitenweiten Filter, deshalb
|
|
* heißt es dort "im gewählten Zeitraum". Auf dem Dashboard gibt es den nicht
|
|
* jede Kachel trägt ihren eigenen Zeitraum und muss ihn benennen können.
|
|
*/
|
|
bezugText,
|
|
icon,
|
|
verlauf,
|
|
akzent = "neutral",
|
|
/** Bei "positiv" ist ein steigender Wert gut, bei "negativ" schlecht. */
|
|
richtungGut = "positiv",
|
|
}: {
|
|
label: string;
|
|
wert: string;
|
|
zusatz?: string;
|
|
trend?: { wert: number; richtung: "up" | "down" | "flat" };
|
|
trendLabel?: string;
|
|
bezug?: Bezug;
|
|
bezugText?: string;
|
|
icon: string;
|
|
verlauf?: number[];
|
|
/**
|
|
* Färbt das Icon. Nur setzen, wenn die Farbe aus einem Schwellenvergleich
|
|
* kommt, ein dekorativ grünes Icon behauptet "gut", ohne es zu belegen
|
|
* (docs/DESIGN-SYSTEM.md §1: Statusfarben nur für Aussagen).
|
|
*/
|
|
akzent?: "neutral" | "positiv" | "warnung" | "negativ" | "info";
|
|
richtungGut?: "positiv" | "negativ";
|
|
}) {
|
|
const gut =
|
|
trend?.richtung === "flat"
|
|
? null
|
|
: richtungGut === "positiv"
|
|
? trend?.richtung === "up"
|
|
: trend?.richtung === "down";
|
|
|
|
const trendFarbe =
|
|
gut === null ? STATUS_FARBEN.neutral : gut ? STATUS_FARBEN.positiv : STATUS_FARBEN.negativ;
|
|
|
|
const akzentFarbe = {
|
|
neutral: CHART.textSekundaer,
|
|
positiv: STATUS_FARBEN.positiv,
|
|
warnung: STATUS_FARBEN.warnung,
|
|
negativ: STATUS_FARBEN.negativ,
|
|
info: STATUS_FARBEN.info,
|
|
}[akzent];
|
|
|
|
return (
|
|
/* h-full: Die Karte füllt ihre Rasterzelle. Ohne das ist die Karte mit
|
|
Trendzeile und Verlaufskurve höher als ihre Nachbarn, und eine Reihe
|
|
gleichartiger Kennzahlen bekommt eine ungleiche Unterkante: auf dem
|
|
Dashboard sichtbar, weil dort ein Link zwischen Zelle und Karte steht
|
|
und die Streckung des Rasters nicht mehr durchreicht. */
|
|
<article className="flex h-full min-h-[136px] flex-col rounded-lg border border-solid border-border-default bg-white p-4 shadow-sm transition-shadow duration-200 hover:shadow-md">
|
|
<div className="flex items-start justify-between gap-2">
|
|
{/* min-w-0 + break-words: Ohne das schiebt ein langes Label wie
|
|
„Umsatz (Zahlungseingang)" das Icon aus der Karte und laeuft selbst
|
|
ueber den Rand. Die Karte wird dadurch eine Zeile hoeher, das ist
|
|
der Preis dafuer, dass die Beschriftung vollstaendig lesbar bleibt
|
|
(docs/DESIGN-SYSTEM.md §9). */}
|
|
<span className="min-w-0 break-words text-[12px] font-medium leading-tight text-text-neutral-tertiary">
|
|
{label}
|
|
</span>
|
|
<span
|
|
className="flex h-8 w-8 flex-none items-center justify-center rounded-md bg-neutral-50"
|
|
style={{ color: akzentFarbe }}
|
|
>
|
|
<FeatherIcon name={icon} className="text-[15px]" />
|
|
</span>
|
|
</div>
|
|
|
|
<div className="mt-3 flex flex-col gap-1">
|
|
{/* "277.460 €" und "34,2 Tage" tragen ihre Einheit als eigenes Wort.
|
|
Ohne nowrap bricht die schmale Karte zwischen Betrag und Einheit um:
|
|
Die Zahl liest sich dann einen Augenblick lang als Wert ohne Einheit,
|
|
und die Karte wächst um eine Zeile, wodurch die Reihe ihre gemeinsame
|
|
Grundlinie verliert. */}
|
|
<span className="whitespace-nowrap text-[28px] font-semibold leading-none tracking-[-0.02em] text-text-neutral-primary">
|
|
{wert}
|
|
</span>
|
|
{zusatz ? (
|
|
<span className="text-[12px] leading-snug text-text-neutral-tertiary">{zusatz}</span>
|
|
) : null}
|
|
</div>
|
|
|
|
{/* Fußzeile: immer vorhanden, damit die Reihe eine gemeinsame Grundlinie hat. */}
|
|
<div className="mt-auto flex items-end justify-between gap-3 pt-3">
|
|
{trend ? (
|
|
<span
|
|
className="flex min-w-0 items-center gap-1 whitespace-nowrap text-[12px] font-medium"
|
|
style={{ color: trendFarbe }}
|
|
>
|
|
{trend.richtung === "up" ? (
|
|
<FeatherArrowUpRight className="text-[13px] flex-none" />
|
|
) : trend.richtung === "down" ? (
|
|
<FeatherArrowDownRight className="text-[13px] flex-none" />
|
|
) : (
|
|
<FeatherMinus className="text-[13px] flex-none" />
|
|
)}
|
|
{trend.richtung === "flat"
|
|
? "unverändert"
|
|
: `${trend.wert.toFixed(1).replace(".", ",")} %`}
|
|
<span className="hidden truncate font-normal text-text-neutral-tertiary xl:inline">
|
|
{trendLabel}
|
|
</span>
|
|
</span>
|
|
) : (
|
|
<span className="text-[11px] leading-tight text-text-neutral-tertiary">
|
|
{bezugText ?? BEZUG_LABEL[bezug]}
|
|
</span>
|
|
)}
|
|
|
|
{verlauf && verlauf.length > 1 ? (
|
|
<span className="h-8 w-16 flex-none">
|
|
{/* Die Kurve trägt dieselbe Aussage wie der Pfeil daneben und
|
|
deshalb auch dieselbe Farbe, sonst zeigt die Karte zwei
|
|
widersprüchliche Signale. */}
|
|
<Sparkline werte={verlauf} farbe={trend ? trendFarbe : CHART.achse} />
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
</article>
|
|
);
|
|
}
|