Files
Dashboard/features/dashboard/pages/FinanzenSeite.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

257 lines
8.4 KiB
TypeScript
Raw 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 { 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>
<div className="grid grid-cols-1 gap-4 xl:grid-cols-3">
<ChartCard
id="finanzen-verlauf"
titel="Zahlungseingang im Zeitverlauf"
untertitel="Aktueller Zeitraum gegen Vorjahr"
hinweis="Grundlage sind bezahlte Ausgangsrechnungen zum Zahlungsdatum."
className="xl:col-span-2"
legende={
<ChartLegende
eintraege={[
{ name: "Aktueller Zeitraum", farbe: PRIMAER_FARBE },
{ name: "Vorjahr", farbe: VERGLEICH_FARBE, gestrichelt: true },
]}
/>
}
>
<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>
</div>
<div className="grid grid-cols-1 items-start gap-4 lg:grid-cols-3">
<ChartCard
className="lg:col-span-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>
</div>
<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>
</>
);
}