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
257 lines
8.4 KiB
TypeScript
257 lines
8.4 KiB
TypeScript
"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>
|
||
</>
|
||
);
|
||
}
|