Files
dejanandClaude Opus 5 bada5db344 Menue nicht mehr abschneiden, Zeile teilt sich nach dem Ausblenden neu auf
Zwei Fehler aus dem Sparring, beide an der Diagrammkarte.

**Das Optionsmenue war abgeschnitten.** Die Karte trug `overflow-hidden`, das
Menue klappt aus dem Kopf heraus, und der zweite Eintrag war nur zur Haelfte
zu sehen. Das Beschneiden ist entfallen; die runden Ecken brauchen es nicht,
weil Kopf und Koerper 16 px Innenabstand haben und nichts an der Kante anliegt.

**Nach dem Ausblenden blieb eine Luecke.** Die Zeilen waren ein Raster mit
festen Spalten. Blendet man eine Karte aus, gibt ChartCard `null` zurueck, die
Spalte bleibt aber stehen und die Nachbarkarte behaelt ihre zwei Drittel.

Neu ist `ChartReihe` in PageContainer.tsx: eine Flexzeile statt eines Rasters.
Die verbliebenen Karten verteilen den Platz unter sich neu, ohne dass jemand
mitzaehlen muesste, wie viele gerade sichtbar sind. Nachgemessen auf
/analytics: Der Zahlungsverlauf geht von 574 px auf 1164 px, sobald die
Nachbarkachel verschwindet.

Zehn Zeilen auf allen fuenf Analyse-Seiten umgestellt. `gleichHoch` haelt die
Unterscheidung aus dem letzten Sparring fest: gestreckt nur, wo die Karten
dieselbe Bauart haben.

Beim Umbau eine Regression gebaut und durch Nachmessen gefunden: Aus 885/443
wurde 664/664. `[&>*]:flex-1` erzeugt den Selektor `.zeile > *`, und der ist
spezifischer als eine Klasse am Kind, also verlor `xl:flex-[2]`. Jetzt
`xl:flex-[2]!`, mit dem Grund als Kommentar an der Komponente.

Nachgemessen nach der Aenderung, alle Zeilen: 885/443 wo zwei zu eins gewollt
ist, 389/389 und 409/409 und 595/595 wo gleiche Hoehe gewollt ist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPeZNwSeU7qG3RRCwaxEZX
2026-08-31 23:09:39 +02:00

257 lines
8.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import React from "react";
import { ChartReihe, PageContainer, Raster } from "../components/layout/PageContainer";
import { KPICard } from "../components/cards/KPICard";
import { ChartCard } from "../components/cards/ChartCard";
import { StatZeile } from "../components/cards/StatZeile";
import { AreaVerlauf } from "../components/charts/AreaVerlauf";
import { BalkenVertikal } from "../components/charts/BalkenVertikal";
import { BalkenHorizontal } from "../components/charts/BalkenHorizontal";
import { Donut } from "../components/charts/Donut";
import { ChartLegende } from "../components/charts/ChartLegende";
import { DataTable, type Spalte } from "../components/tables/DataTable";
import { RechnungBadge } from "../components/tables/StatusBadge";
import {
berechneTrend,
formatDatum,
formatEuro,
formatEuroAchse,
formatEuroKompakt,
formatProzent,
formatTage,
tageSeit,
tageLabel,
} from "../utils/formatters";
import { HEUTE } from "../utils/dateUtils";
import {
PRIMAER_FARBE,
RISIKO_RAMPE,
STATUS_TEXT,
VERGLEICH_FARBE,
} from "../utils/chartTheme";
import type { Rechnung } from "../data/types";
import { SeitenZustand } from "../components/layout/SeitenZustand";
import type { VMState } from "../view-models";
import type { FinanzenDaten } from "../view-models/finanzen.vm";
/** Ampelfarben für die Altersstruktur: je älter die Forderung, desto kritischer. */
const ALTER_FARBEN = [...RISIKO_RAMPE];
export function FinanzenSeite({ vm }: { vm: VMState<FinanzenDaten> }) {
if (vm.status !== "ready") {
return <SeitenZustand vm={vm} />;
}
const d = vm.data;
const spalten: Spalte<Rechnung>[] = [
{
key: "nummer",
titel: "Rechnung",
zelle: (r) => (
<div className="flex flex-col">
<span className="font-medium">{r.nummer}</span>
<span className="text-[11px] text-text-neutral-tertiary">
{d.kundeName.get(r.kundeId) ?? "Unbekannt"}
</span>
</div>
),
sortWert: (r) => r.nummer,
breite: "30%",
},
{ key: "status", titel: "Status", zelle: (r) => <RechnungBadge status={r.status} /> },
{
key: "gestellt",
titel: "Gestellt",
zelle: (r) => formatDatum(r.gestelltAm),
sortWert: (r) => r.gestelltAm,
},
{
key: "faellig",
titel: "Überfällig",
numerisch: true,
zelle: (r) => {
const tage = tageSeit(r.faelligAm, HEUTE);
return tage > 0 ? (
<span
className={tage > 30 ? "font-medium" : ""}
style={{ color: tage > 30 ? STATUS_TEXT.negativ : STATUS_TEXT.warnung }}
>
{tageLabel(tage)}
</span>
) : (
<span className="text-text-neutral-tertiary">
in {Math.abs(tage) === 1 ? "1 Tag" : `${Math.abs(tage)} Tagen`}
</span>
);
},
sortWert: (r) => tageSeit(r.faelligAm, HEUTE),
},
{
key: "betrag",
titel: "Betrag",
numerisch: true,
zelle: (r) => <span className="font-medium">{formatEuro(r.betrag)}</span>,
sortWert: (r) => r.betrag,
},
];
return (
<>
<PageContainer>
<Raster spalten={5}>
<KPICard
label="Umsatz (bezahlt)"
wert={formatEuro(d.umsatzAktuell)}
zusatz="Zahlungseingang im Zeitraum"
trend={berechneTrend(d.umsatzAktuell, d.umsatzVorher)}
icon="Euro"
verlauf={d.sparkline}
/>
<KPICard
label="Offene Forderungen"
wert={formatEuro(d.posten.gesamt)}
zusatz={`${d.posten.anzahl} Rechnungen`}
icon="FileText"
bezug="stichtag"
/>
<KPICard
label="Überfällig"
wert={formatEuro(d.posten.ueberfaelligBetrag)}
zusatz={`${d.posten.ueberfaelligAnzahl} Rechnungen im Mahnlauf`}
icon="AlertTriangle"
bezug="stichtag"
akzent={d.posten.ueberfaelligBetrag > 0 ? "negativ" : "positiv"}
/>
<KPICard
label="Ø Zahlungsdauer"
wert={formatTage(d.zahlungsdauerTage)}
zusatz="Rechnung bis Zahlungseingang"
icon="Clock"
akzent={d.zahlungsdauerTage > 30 ? "warnung" : "positiv"}
richtungGut="negativ"
/>
<KPICard
label="Deckungsbeitrag"
wert={formatProzent(d.margeProzent)}
zusatz="Gewichtet über Abschlüsse"
icon="Percent"
/>
</Raster>
<ChartReihe umbruch="xl" gleichHoch>
<ChartCard
id="finanzen-verlauf"
titel="Zahlungseingang im Zeitverlauf"
untertitel="Aktueller Zeitraum gegen Vorjahr"
hinweis="Grundlage sind bezahlte Ausgangsrechnungen zum Zahlungsdatum."
className="xl:flex-[2]!"
legende={
<ChartLegende
eintraege={[
{ name: "Aktueller Zeitraum", farbe: PRIMAER_FARBE },
{ name: "Vorjahr", farbe: VERGLEICH_FARBE },
]}
/>
}
>
<AreaVerlauf
daten={d.verlauf}
formatWert={formatEuroKompakt}
formatAchse={formatEuroAchse}
zeigeVergleich
hoehe={250}
/>
<StatZeile
spalten={2}
eintraege={[
{ label: "Vorperiode", wert: formatEuro(d.umsatzVorher) },
{
label: "Veränderung",
wert: `${d.umsatzAktuell >= d.umsatzVorher ? "+" : "−"}${formatEuro(Math.abs(d.umsatzAktuell - d.umsatzVorher))}`,
},
]}
/>
</ChartCard>
<ChartCard
id="finanzen-status"
titel="Offener Bestand nach Status"
untertitel="Wie viel des offenen Bestands bereits überfällig ist"
hinweis="Bezugsgröße ist ausschließlich der offene Bestand zum Stichtag. Bezahlte Rechnungen sind nicht enthalten, sie würden über 90 % des Rings belegen und die Aussage erdrücken."
>
<Donut
daten={d.offenNachStatus}
formatWert={formatEuroKompakt}
hoehe={200}
mitteLabel="Offen"
formatMitte={formatEuro}
/>
</ChartCard>
</ChartReihe>
<ChartReihe umbruch="lg">
<ChartCard
className="lg:flex-[2]!"
id="finanzen-alter"
titel="Offene Forderungen nach Alter"
untertitel={`Gesamt ${formatEuro(d.posten.gesamt)}`}
hinweis="Alter ab Fälligkeitsdatum. Ab 60 Tagen sinkt die Einbringungsquote erfahrungsgemäß deutlich."
>
<BalkenVertikal
daten={d.posten.nachAlter}
formatWert={formatEuroKompakt}
formatAchse={formatEuroAchse}
hoehe={240}
farbenProBalken={ALTER_FARBEN}
/>
<StatZeile
spalten={2}
eintraege={[
{
label: "Älter als 90 Tage",
wert: formatEuro(
d.posten.nachAlter.find((b) => b.name === "> 90 Tage")?.value ?? 0
),
hinweis: "Abschreibungsrisiko",
},
{ label: "Überfällig gesamt", wert: formatEuro(d.posten.ueberfaelligBetrag) },
]}
/>
</ChartCard>
<ChartCard
id="finanzen-gewerke"
titel="Umsatz nach Gewerk"
untertitel="Zahlungseingang im Zeitraum"
>
<BalkenHorizontal
daten={d.gewerkUmsatz}
formatWert={formatEuroKompakt}
einfarbig={false}
maxBalken={5}
/>
</ChartCard>
</ChartReihe>
<ChartCard
id="finanzen-offene-liste"
titel={`Offene Rechnungen (${d.posten.anzahl})`}
untertitel="Sortiert nach Fälligkeit, die Arbeitsliste für den Mahnlauf"
>
<DataTable
zeilen={d.posten.liste}
spalten={spalten}
proSeite={8}
suchFelder={(r) => `${r.nummer} ${d.kundeName.get(r.kundeId) ?? ""}`}
leerText="Keine offenen Rechnungen"
/>
</ChartCard>
</PageContainer>
</>
);
}