/** * View-Model der Auftragsseite. * * 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 { abschlussquote, aktiveAuftraege, auftraegeImZeitraum, auftraegeNachGewerk, auftraegeNachStatus, auftragseingang, durchlaufzeit, foerderQuote, ueberfaelligeAuftraege, vertriebsTrichter, } from "../data/calculations"; import type { Auftrag, ChartPunkt, Datensatz, FilterState, ZeitreihePunkt, } from "../data/types"; import { imFenster, vorperiode } from "../utils/dateUtils"; import { bereit, leer, type VMState } from "./index"; export interface AuftraegeDaten { readonly imZeitraum: Auftrag[]; readonly vorher: Auftrag[]; readonly aktiv: Auftrag[]; readonly ueberfaellig: Auftrag[]; readonly quote: number; readonly laufzeit: number; readonly eingang: ZeitreihePunkt[]; readonly sparkline: number[]; readonly bestand: number; readonly durchschnittAuftragswert: number; readonly trichter: ChartPunkt[]; readonly statusVerteilung: ChartPunkt[]; readonly gewerkAnzahl: ChartPunkt[]; readonly foerderung: ReturnType; readonly foerderungJeAuftrag: number; /** * Namen je Id. Der Auftrag trägt nur die Ids; die Auflösung gehört zu den * Daten, nicht in die Zellen-Funktion der Tabelle. */ readonly mitarbeiterName: ReadonlyMap; readonly kundeName: ReadonlyMap; } export function baueAuftraegeVM(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 imZeitraum = auftraegeImZeitraum(daten.auftraege, filter.zeitraum); const aktiv = aktiveAuftraege(daten.auftraege); const eingang = auftragseingang(daten.auftraege, filter.zeitraum); const foerderung = foerderQuote(daten.auftraege); return bereit({ imZeitraum, vorher: daten.auftraege.filter((a) => imFenster(a.angelegtAm, vorperiode(filter.zeitraum))), aktiv, ueberfaellig: ueberfaelligeAuftraege(daten.auftraege), quote: abschlussquote(daten.auftraege), laufzeit: durchlaufzeit(daten.auftraege), eingang, sparkline: eingang.slice(-10).map((p) => p.value), bestand: aktiv.reduce((s, a) => s + a.wert, 0), durchschnittAuftragswert: imZeitraum.length ? imZeitraum.reduce((s, a) => s + a.wert, 0) / imZeitraum.length : 0, trichter: vertriebsTrichter(daten.auftraege), statusVerteilung: auftraegeNachStatus(daten.auftraege), gewerkAnzahl: auftraegeNachGewerk(daten.auftraege), foerderung, foerderungJeAuftrag: foerderung.bewilligt ? foerderung.summe / foerderung.bewilligt : 0, mitarbeiterName: new Map(daten.mitarbeiter.map((m) => [m.id, m.name])), kundeName: new Map(daten.kunden.map((k) => [k.id, k.name])), }); }