/** * View-Model der Finanzseite. * * Reine Funktion über typisierten Eingangsdaten, kein React, kein Store, kein * Zugriff auf `DATENSATZ`. Genau diese Grenze macht den Einbau billig: In der * Software ruft der Loader denselben Builder mit echten Daten auf, die * Screen-Komponente bleibt unverändert. */ import { marge, offeneNachStatus, offenePosten, umsatz, umsatzNachGewerk, umsatzVerlauf, umsatzVorperiode, zahlungsdauer, type OffenePosten, } from "../data/calculations"; import type { ChartPunkt, Datensatz, FilterState, ZeitreihePunkt } from "../data/types"; import { bereit, leer, type VMState } from "./index"; export interface FinanzenDaten { readonly umsatzAktuell: number; readonly umsatzVorher: number; readonly verlauf: ZeitreihePunkt[]; readonly sparkline: number[]; readonly posten: OffenePosten; readonly zahlungsdauerTage: number; readonly margeProzent: number; readonly offenNachStatus: ChartPunkt[]; readonly gewerkUmsatz: ChartPunkt[]; /** * Kundenname je Id. Die Rechnung trägt nur die Id; die Auflösung gehört zu den * Daten, nicht in die Zellen-Funktion der Tabelle. */ readonly kundeName: ReadonlyMap; } export function baueFinanzenVM(daten: Datensatz, filter: FilterState): VMState { // Kein Auftrag und keine Rechnung heisst: Die Filter schneiden alles weg. // Das ist ein Leerzustand, kein Fehler. if (!daten.auftraege.length && !daten.rechnungen.length) return leer(); const verlauf = umsatzVerlauf(daten.rechnungen, filter.zeitraum); return bereit({ umsatzAktuell: umsatz(daten.rechnungen, filter.zeitraum), umsatzVorher: umsatzVorperiode(daten.rechnungen, filter.zeitraum), verlauf, sparkline: verlauf.slice(-10).map((p) => p.value), posten: offenePosten(daten.rechnungen), zahlungsdauerTage: zahlungsdauer(daten.rechnungen, filter.zeitraum), margeProzent: marge(daten.auftraege, filter.zeitraum), offenNachStatus: offeneNachStatus(daten.rechnungen), gewerkUmsatz: umsatzNachGewerk(daten.auftraege, daten.rechnungen, filter.zeitraum), kundeName: new Map(daten.kunden.map((k) => [k.id, k.name])), }); }