Files
Dashboard/features/dashboard/components/cards/KPICard.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

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