/** * View-Model der Teamseite. * * 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 { teamKennzahlen, type MitarbeiterKennzahl } from "../data/calculations"; import type { ChartPunkt, Datensatz, FilterState } from "../data/types"; import { imFenster, zeitfenster } from "../utils/dateUtils"; import { bereit, leer, type VMState } from "./index"; /** * Tabellenzeile der Mitarbeiterübersicht. * * Die `id` hängt hier und nicht in der Seite, weil die Tabelle einen stabilen * Schlüssel braucht, das ist eine Eigenschaft des Datensatzes, keine der * Darstellung. */ export type TeamZeile = MitarbeiterKennzahl & { readonly id: string }; export interface TeamDaten { readonly mitarbeiterGesamt: number; readonly aktive: MitarbeiterKennzahl[]; readonly durchschnittAuslastung: number; readonly stundenGesamt: number; readonly produktivGesamt: number; readonly produktivQuote: number; readonly abwesenheitStunden: number; readonly abwesenheitNach: ChartPunkt[]; readonly zeilen: TeamZeile[]; } /** Anzeigenamen der Abwesenheitsgründe, die Rohwerte sind Kleinschreibung. */ const ABWESENHEIT_LABEL: Record = { urlaub: "Urlaub", krank: "Krank", fortbildung: "Fortbildung", }; export function baueTeamVM(daten: Datensatz, filter: FilterState): VMState { // Kein Mitarbeiter in der Auswahl heisst: Die Filter schneiden alles weg. // Das ist ein Leerzustand, kein Fehler. if (!daten.mitarbeiter.length) return leer(); const kennzahlen = teamKennzahlen( daten.mitarbeiter, daten.zeiten, daten.auftraege, filter.zeitraum ); const aktive = kennzahlen.filter((k) => k.mitarbeiter.status === "aktiv"); // Kennzahlen beziehen sich auf die aktiven Kräfte: Ausgetretene hätten null // Stunden und würden jeden Durchschnitt verzerren. const stundenGesamt = aktive.reduce((s, k) => s + k.stunden, 0); const produktivGesamt = aktive.reduce((s, k) => s + k.produktivStunden, 0); const fenster = zeitfenster(filter.zeitraum); const abwesenheiten = daten.zeiten.filter( (z) => z.art === "abwesenheit" && imFenster(z.datum, fenster) ); const zaehler = new Map(); for (const eintrag of abwesenheiten) { const name = ABWESENHEIT_LABEL[eintrag.abwesenheitsgrund ?? "sonstige"] ?? "Sonstige"; zaehler.set(name, (zaehler.get(name) ?? 0) + eintrag.stunden); } return bereit({ mitarbeiterGesamt: kennzahlen.length, aktive, durchschnittAuslastung: aktive.length ? aktive.reduce((s, k) => s + k.auslastungProzent, 0) / aktive.length : 0, stundenGesamt, produktivGesamt, produktivQuote: stundenGesamt > 0 ? (produktivGesamt / stundenGesamt) * 100 : 0, abwesenheitStunden: abwesenheiten.reduce((s, a) => s + a.stunden, 0), abwesenheitNach: [...zaehler.entries()].map(([name, value]) => ({ name, value })), zeilen: kennzahlen.map((k) => ({ ...k, id: k.mitarbeiter.id })), }); }