Files
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

569 lines
20 KiB
TypeScript
Raw Permalink 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.
/**
* Kennzahlen-Berechnungen.
*
* Alle Funktionen sind pure functions über dem gefilterten Datensatz
* (Anforderung INFRA-06): die Entwicklung kann sie unverändert übernehmen
* und lediglich die Datenquelle austauschen.
*
* Die Definition jeder Kennzahl steht in docs/KPI-KATALOG.md.
*/
import {
AUFTRAG_STATUS_LABEL,
FUNNEL_STUFEN,
RECHNUNG_STATUS_LABEL,
type Auftrag,
type AuftragStatus,
type ChartPunkt,
type Datensatz,
type Kunde,
type Rechnung,
FOERDERFAEHIGE_KATEGORIEN,
type ZeitraumPreset,
type ZeitreihePunkt,
type Zeiteintrag,
type Mitarbeiter,
} from "./types";
import {
HEUTE,
bucketKey,
bucketsFuer,
formatWoche,
imFenster,
vorperiode,
zeitfenster,
addMonate,
} from "../utils/dateUtils";
import { formatDatumKurz, formatMonatKurz, tageSeit } from "../utils/formatters";
/* ------------------------------------------------------------- Umsatz */
/** Umsatz = Summe bezahlter Ausgangsrechnungen im Zeitraum (Zahlungseingang). */
export function umsatz(rechnungen: Rechnung[], zeitraum: ZeitraumPreset): number {
const fenster = zeitfenster(zeitraum);
return rechnungen
.filter((r) => r.status === "bezahlt" && imFenster(r.bezahltAm, fenster))
.reduce((summe, r) => summe + r.betrag, 0);
}
/** Gleiche Berechnung für die Vorperiode: Basis der Trend-Badges. */
export function umsatzVorperiode(rechnungen: Rechnung[], zeitraum: ZeitraumPreset): number {
const fenster = vorperiode(zeitraum);
return rechnungen
.filter((r) => r.status === "bezahlt" && imFenster(r.bezahltAm, fenster))
.reduce((summe, r) => summe + r.betrag, 0);
}
/** Beschriftung eines Buckets, eine Stelle fuer alle drei Zeitreihen. */
function bucketLabel(key: string, art: "tag" | "woche" | "monat"): string {
if (art === "monat") return formatMonatKurz(key);
if (art === "woche") return formatWoche(key);
return formatDatumKurz(key);
}
/**
* Umsatzverlauf mit Vorjahresvergleich.
* Buckets richten sich nach dem Zeitraum (Tage bei ≤ 30 Tagen, sonst Monate).
*/
export function umsatzVerlauf(
rechnungen: Rechnung[],
zeitraum: ZeitraumPreset
): ZeitreihePunkt[] {
const { art, schluessel } = bucketsFuer(zeitraum);
const aktuell = new Map<string, number>();
const vorjahr = new Map<string, number>();
for (const r of rechnungen) {
if (r.status !== "bezahlt" || !r.bezahltAm) continue;
aktuell.set(
bucketKey(r.bezahltAm, art),
(aktuell.get(bucketKey(r.bezahltAm, art)) ?? 0) + r.betrag
);
// Vorjahreswert: dasselbe Datum plus 12 Monate landet im aktuellen Bucket.
const verschoben = addMonate(new Date(r.bezahltAm), 12);
const key = bucketKey(verschoben.toISOString().slice(0, 10), art);
vorjahr.set(key, (vorjahr.get(key) ?? 0) + r.betrag);
}
return schluessel.map((key) => ({
datum: key,
label: bucketLabel(key, art),
value: aktuell.get(key) ?? 0,
vergleich: vorjahr.get(key) ?? 0,
}));
}
/** Deckungsbeitrag: gewichtete Marge über abgeschlossene Aufträge im Zeitraum. */
export function marge(auftraege: Auftrag[], zeitraum: ZeitraumPreset): number {
const fenster = zeitfenster(zeitraum);
const relevant = auftraege.filter(
(a) => a.status === "abgeschlossen" && imFenster(a.abgeschlossenAm, fenster)
);
const gesamtwert = relevant.reduce((s, a) => s + a.wert, 0);
if (gesamtwert === 0) return 0;
const db = relevant.reduce((s, a) => s + (a.wert * a.margeProzent) / 100, 0);
return (db / gesamtwert) * 100;
}
/* --------------------------------------------------------- Rechnungen */
export interface OffenePosten {
gesamt: number;
anzahl: number;
ueberfaelligBetrag: number;
ueberfaelligAnzahl: number;
nachAlter: ChartPunkt[];
liste: Rechnung[];
}
/**
* Offene Posten sind stichtagsbezogen (nicht zeitraumgefiltert):
* Was ist heute noch nicht bezahlt?
*/
export function offenePosten(rechnungen: Rechnung[]): OffenePosten {
const offen = rechnungen.filter((r) => r.status === "gestellt" || r.status === "ueberfaellig");
const buckets: Record<string, { betrag: number; anzahl: number }> = {
"0–30 Tage": { betrag: 0, anzahl: 0 },
"31–60 Tage": { betrag: 0, anzahl: 0 },
"61–90 Tage": { betrag: 0, anzahl: 0 },
"> 90 Tage": { betrag: 0, anzahl: 0 },
};
for (const r of offen) {
const tage = tageSeit(r.faelligAm, HEUTE);
const bucket =
tage <= 30 ? "0–30 Tage" : tage <= 60 ? "31–60 Tage" : tage <= 90 ? "61–90 Tage" : "> 90 Tage";
buckets[bucket].betrag += r.betrag;
buckets[bucket].anzahl += 1;
}
const ueberfaellig = offen.filter((r) => r.status === "ueberfaellig");
return {
gesamt: offen.reduce((s, r) => s + r.betrag, 0),
anzahl: offen.length,
ueberfaelligBetrag: ueberfaellig.reduce((s, r) => s + r.betrag, 0),
ueberfaelligAnzahl: ueberfaellig.length,
nachAlter: Object.entries(buckets).map(([name, v]) => ({ name, value: v.betrag })),
liste: offen.sort((a, b) => (a.faelligAm < b.faelligAm ? -1 : 1)),
};
}
/** Durchschnittliche Zahlungsdauer in Tagen (gestellt → bezahlt). */
export function zahlungsdauer(rechnungen: Rechnung[], zeitraum: ZeitraumPreset): number {
const fenster = zeitfenster(zeitraum);
const bezahlt = rechnungen.filter(
(r) => r.status === "bezahlt" && r.bezahltAm && imFenster(r.bezahltAm, fenster)
);
if (!bezahlt.length) return 0;
const summe = bezahlt.reduce(
(s, r) =>
s + (new Date(r.bezahltAm!).getTime() - new Date(r.gestelltAm).getTime()) / 86_400_000,
0
);
return summe / bezahlt.length;
}
/**
* Zusammensetzung des OFFENEN Bestands.
*
* Nicht zu verwechseln mit rechnungenNachStatus(): dort steht der kumulierte
* Umsatz aus zwei Jahren gegen den heutigen Forderungsbestand, dabei entfallen
* über 90 % auf "Bezahlt", und der Ring zeigt nichts mehr. Der offene Bestand
* ist ein echtes Ganzes: alles, was heute noch aussteht.
*/
export function offeneNachStatus(rechnungen: Rechnung[]): ChartPunkt[] {
// Entwuerfe sind noch nicht gestellt und damit keine Forderung. Wuerden sie
// mitzaehlen, waeche die Ringsumme von der KPI "Offene Forderungen" ab.
const offen = rechnungen.filter(
(r) => r.richtung === "ausgang" && (r.status === "gestellt" || r.status === "ueberfaellig")
);
const summen = new Map<string, number>();
for (const r of offen) {
const label = RECHNUNG_STATUS_LABEL[r.status];
summen.set(label, (summen.get(label) ?? 0) + r.betrag);
}
return [...summen.entries()].map(([name, value]) => ({ name, value }));
}
export function rechnungenNachStatus(rechnungen: Rechnung[]): ChartPunkt[] {
const zaehler = new Map<string, number>();
for (const r of rechnungen) {
const label = RECHNUNG_STATUS_LABEL[r.status];
zaehler.set(label, (zaehler.get(label) ?? 0) + r.betrag);
}
return [...zaehler.entries()].map(([name, value]) => ({ name, value }));
}
/* ------------------------------------------------------------ Aufträge */
export function auftraegeImZeitraum(auftraege: Auftrag[], zeitraum: ZeitraumPreset): Auftrag[] {
const fenster = zeitfenster(zeitraum);
return auftraege.filter((a) => imFenster(a.angelegtAm, fenster));
}
/** "Aktiv" = beauftragt oder in Arbeit: das, was das Team gerade bindet. */
export function aktiveAuftraege(auftraege: Auftrag[]): Auftrag[] {
return auftraege.filter((a) => a.status === "beauftragt" || a.status === "in_arbeit");
}
export function ueberfaelligeAuftraege(auftraege: Auftrag[]): Auftrag[] {
// Einen Termin gibt es erst ab der Beauftragung, eine offene Anfrage kann
// nicht "über Termin" sein.
return auftraege
.filter(
(a) =>
(a.status === "beauftragt" || a.status === "in_arbeit") &&
new Date(a.faelligAm) < HEUTE
)
.sort((a, b) => (a.faelligAm < b.faelligAm ? -1 : 1));
}
export function auftraegeNachStatus(auftraege: Auftrag[]): ChartPunkt[] {
const zaehler = new Map<AuftragStatus, number>();
for (const a of auftraege) zaehler.set(a.status, (zaehler.get(a.status) ?? 0) + 1);
return (Object.keys(AUFTRAG_STATUS_LABEL) as AuftragStatus[])
.map((status) => ({ name: AUFTRAG_STATUS_LABEL[status], value: zaehler.get(status) ?? 0 }))
.filter((p) => p.value > 0);
}
export function auftraegeNachGewerk(auftraege: Auftrag[]): ChartPunkt[] {
const zaehler = new Map<string, number>();
for (const a of auftraege) zaehler.set(a.gewerk, (zaehler.get(a.gewerk) ?? 0) + 1);
return [...zaehler.entries()]
.map(([name, value]) => ({ name, value }))
.sort((a, b) => b.value - a.value);
}
export function umsatzNachGewerk(
auftraege: Auftrag[],
rechnungen: Rechnung[],
zeitraum: ZeitraumPreset
): ChartPunkt[] {
const fenster = zeitfenster(zeitraum);
const gewerkProAuftrag = new Map(auftraege.map((a) => [a.id, a.gewerk]));
const summen = new Map<string, number>();
for (const r of rechnungen) {
if (r.status !== "bezahlt" || !imFenster(r.bezahltAm, fenster)) continue;
const gewerk = gewerkProAuftrag.get(r.auftragId);
if (!gewerk) continue;
summen.set(gewerk, (summen.get(gewerk) ?? 0) + r.betrag);
}
return [...summen.entries()]
.map(([name, value]) => ({ name, value }))
.sort((a, b) => b.value - a.value);
}
/**
* Vertriebstrichter: Wie viele Vorgänge haben eine Stufe mindestens erreicht?
* Verlorene Aufträge zählen nur bis zu ihrer letzten erreichten Stufe.
*/
/**
* Vertriebstrichter: bewusst nur über das Angebotsgeschäft.
*
* Wartung, Notdienst und E-Check laufen ohne Angebot direkt in die Ausführung.
* Zählte man sie mit, würde der Trichter unten breiter als oben und die
* Übergangsquoten wären wertlos.
*/
export function vertriebsTrichter(auftraege: Auftrag[]): ChartPunkt[] {
const rang = (status: AuftragStatus) => FUNNEL_STUFEN.indexOf(status);
const angebotsgeschaeft = auftraege.filter((a) => a.ueberAngebot);
return FUNNEL_STUFEN.map((stufe, index) => {
const anzahl = angebotsgeschaeft.filter((a) => {
// Verlorene Vorgänge zählen bis zu der Stufe, auf der sie abgesprungen sind.
if (a.status === "verloren") {
return a.verlorenAb ? rang(a.verlorenAb) >= index : false;
}
return rang(a.status) >= index;
}).length;
return { name: AUFTRAG_STATUS_LABEL[stufe], value: anzahl };
});
}
export function abschlussquote(auftraege: Auftrag[]): number {
// Bezugsgröße ist ausschließlich das Angebotsgeschäft. Wartungs- und
// Notdienstaufträge laufen ohne Angebot und würden die Quote schönrechnen.
const mitAngebot = auftraege.filter(
(a) =>
a.ueberAngebot &&
(["angebot", "beauftragt", "in_arbeit", "abgeschlossen"].includes(a.status) ||
(a.status === "verloren" && a.verlorenAb === "angebot"))
);
if (!mitAngebot.length) return 0;
const gewonnen = mitAngebot.filter((a) =>
["beauftragt", "in_arbeit", "abgeschlossen"].includes(a.status)
).length;
return (gewonnen / mitAngebot.length) * 100;
}
/** Ø Durchlaufzeit abgeschlossener Aufträge in Tagen. */
export function durchlaufzeit(auftraege: Auftrag[]): number {
const fertig = auftraege.filter((a) => a.status === "abgeschlossen" && a.abgeschlossenAm);
if (!fertig.length) return 0;
const summe = fertig.reduce(
(s, a) =>
s + (new Date(a.abgeschlossenAm!).getTime() - new Date(a.angelegtAm).getTime()) / 86_400_000,
0
);
return summe / fertig.length;
}
export function auftragseingang(auftraege: Auftrag[], zeitraum: ZeitraumPreset): ZeitreihePunkt[] {
const { art, schluessel } = bucketsFuer(zeitraum);
const zaehler = new Map<string, number>();
for (const a of auftraege) {
const key = bucketKey(a.angelegtAm, art);
zaehler.set(key, (zaehler.get(key) ?? 0) + 1);
}
return schluessel.map((key) => ({
datum: key,
label: bucketLabel(key, art),
value: zaehler.get(key) ?? 0,
}));
}
/* -------------------------------------------------------------- Kunden */
export function neueKunden(kunden: Kunde[], zeitraum: ZeitraumPreset): number {
const fenster = zeitfenster(zeitraum);
return kunden.filter((k) => imFenster(k.angelegtAm, fenster)).length;
}
/** Gleiches Fenster davor: Basis fuer den Trend an der Neukunden-Karte. */
export function neueKundenVorperiode(kunden: Kunde[], zeitraum: ZeitraumPreset): number {
const fenster = vorperiode(zeitraum);
return kunden.filter((k) => imFenster(k.angelegtAm, fenster)).length;
}
export function kundenEntwicklung(kunden: Kunde[], zeitraum: ZeitraumPreset): ZeitreihePunkt[] {
const { art, schluessel } = bucketsFuer(zeitraum);
const zaehler = new Map<string, number>();
for (const k of kunden) {
const key = bucketKey(k.angelegtAm, art);
zaehler.set(key, (zaehler.get(key) ?? 0) + 1);
}
return schluessel.map((key) => ({
datum: key,
label: bucketLabel(key, art),
value: zaehler.get(key) ?? 0,
}));
}
export function kundenNach(kunden: Kunde[], feld: "quelle" | "typ" | "region"): ChartPunkt[] {
const zaehler = new Map<string, number>();
for (const k of kunden) zaehler.set(k[feld], (zaehler.get(k[feld]) ?? 0) + 1);
return [...zaehler.entries()]
.map(([name, value]) => ({ name, value }))
.sort((a, b) => b.value - a.value);
}
export function topKunden(kunden: Kunde[], anzahl = 5): ChartPunkt[] {
return [...kunden]
.filter((k) => k.umsatzGesamt > 0)
.sort((a, b) => b.umsatzGesamt - a.umsatzGesamt)
.slice(0, anzahl)
.map((k) => ({ name: k.name, value: k.umsatzGesamt }));
}
/** Anteil Kunden mit mehr als einem Auftrag. */
export function wiederkehrquote(kunden: Kunde[]): number {
const mitAuftrag = kunden.filter((k) => k.auftraegeGesamt > 0);
if (!mitAuftrag.length) return 0;
return (mitAuftrag.filter((k) => k.auftraegeGesamt > 1).length / mitAuftrag.length) * 100;
}
/** Kunden, die seit über 12 Monaten keinen Auftrag hatten. */
export function inaktiveKunden(kunden: Kunde[]): Kunde[] {
const grenze = addMonate(HEUTE, -12);
return kunden
.filter((k) => k.auftraegeGesamt > 0 && k.letzterAuftragAm && new Date(k.letzterAuftragAm) < grenze)
.sort((a, b) => b.umsatzGesamt - a.umsatzGesamt);
}
/** Kunden mit auffälliger Zahlungsmoral (> 30 Tage im Schnitt). */
export function zahlungsauffaellig(kunden: Kunde[]): Kunde[] {
return kunden
.filter((k) => k.zahlungsmoralTage > 30 && k.auftraegeGesamt > 0)
.sort((a, b) => b.zahlungsmoralTage - a.zahlungsmoralTage);
}
/* ---------------------------------------------------------------- Team */
export interface MitarbeiterKennzahl {
mitarbeiter: Mitarbeiter;
stunden: number;
produktivStunden: number;
auslastungProzent: number;
abgeschlossen: number;
umsatz: number;
}
/**
* Auslastung = erfasste Arbeitsstunden / verfügbare Stunden im Zeitraum.
* Verfügbare Stunden werden aus den Wochenstunden hochgerechnet.
*/
export function teamKennzahlen(
mitarbeiter: Mitarbeiter[],
zeiten: Zeiteintrag[],
auftraege: Auftrag[],
zeitraum: ZeitraumPreset
): MitarbeiterKennzahl[] {
const fenster = zeitfenster(zeitraum);
const tageImFenster = Math.max(
1,
Math.round((fenster.bis.getTime() - fenster.von.getTime()) / 86_400_000)
);
const wochen = Math.max(1, tageImFenster / 7);
return mitarbeiter
.filter((m) => m.rolle !== "Büro")
.map((m) => {
const eigene = zeiten.filter((z) => z.mitarbeiterId === m.id && imFenster(z.datum, fenster));
const stunden = eigene.reduce((s, z) => s + z.stunden, 0);
const produktiv = eigene
.filter((z) => z.art === "arbeit" && z.auftragId)
.reduce((s, z) => s + z.stunden, 0);
const verfuegbar = m.wochenstunden * wochen;
const eigeneAuftraege = auftraege.filter((a) => a.mitarbeiterId === m.id);
const abgeschlossen = eigeneAuftraege.filter(
(a) => a.status === "abgeschlossen" && imFenster(a.abgeschlossenAm, fenster)
);
return {
mitarbeiter: m,
stunden,
produktivStunden: produktiv,
auslastungProzent: verfuegbar > 0 ? Math.min(135, (produktiv / verfuegbar) * 100) : 0,
abgeschlossen: abgeschlossen.length,
umsatz: abgeschlossen.reduce((s, a) => s + a.wert, 0),
};
})
.sort((a, b) => b.auslastungProzent - a.auslastungProzent);
}
/* -------------------------------------------------------------- Alerts */
export interface AlertEintrag {
id: string;
text: string;
detail: string;
art: "kritisch" | "warnung" | "hinweis";
/** Betrag, um den es geht: macht die Zeilen untereinander vergleichbar. */
betrag?: number;
/**
* Die Handlung, nicht die Beobachtung. Eine Liste ohne Verb ist ein Bericht;
* "Was ist zu tun" braucht ein Verb je Zeile.
*/
aktion: string;
}
/** Handlungsbedarf für die Übersichtsseite. */
export function handlungsbedarf(daten: Datensatz): AlertEintrag[] {
const eintraege: AlertEintrag[] = [];
const posten = offenePosten(daten.rechnungen);
const ueberfaellig = ueberfaelligeAuftraege(daten.auftraege);
const kritischAlt = posten.nachAlter.find((b) => b.name === "> 90 Tage");
if (posten.ueberfaelligAnzahl > 0) {
eintraege.push({
id: "rechnungen-ueberfaellig",
text: `${posten.ueberfaelligAnzahl} überfällige Rechnungen`,
detail: "Zahlungsziel überschritten",
betrag: posten.ueberfaelligBetrag,
aktion: "Mahnlauf starten",
art: "kritisch",
});
}
if (kritischAlt && kritischAlt.value > 0) {
eintraege.push({
id: "rechnungen-90",
text: "Forderungen älter als 90 Tage",
detail: "Einbringungsquote sinkt deutlich",
betrag: kritischAlt.value,
aktion: "Inkasso prüfen",
art: "kritisch",
});
}
if (ueberfaellig.length > 0) {
eintraege.push({
id: "auftraege-ueberfaellig",
text: `${ueberfaellig.length} Aufträge über Termin`,
detail: `Ältester seit ${tageSeit(ueberfaellig[0].faelligAm, HEUTE)} Tagen fällig`,
betrag: ueberfaellig.reduce((s, a) => s + a.wert, 0),
aktion: "Termine neu planen",
art: "warnung",
});
}
const offeneAngebote = daten.auftraege.filter(
(a) => a.status === "angebot" && tageSeit(a.angelegtAm, HEUTE) > 21
);
if (offeneAngebote.length > 0) {
eintraege.push({
id: "angebote-alt",
text: `${offeneAngebote.length} Angebote ohne Rückmeldung`,
detail: "Seit über 21 Tagen offen",
betrag: offeneAngebote.reduce((s, a) => s + a.wert, 0),
aktion: "Nachfassen",
art: "warnung",
});
}
const foerderOffen = daten.auftraege.filter((a) => a.foerderung === "beantragt");
if (foerderOffen.length > 0) {
eintraege.push({
id: "foerderung",
text: `${foerderOffen.length} Förderanträge in Bearbeitung`,
detail: "Bescheid ausstehend",
betrag: foerderOffen.reduce((s, a) => s + a.foerderbetrag, 0),
aktion: "Status prüfen",
art: "hinweis",
});
}
return eintraege;
}
/* --------------------------------------------------------- Förderung */
/**
* Förderquote BAFA/KfW.
*
* Bezugsgröße sind ausschließlich förderfähige Leistungen (Wärmepumpe, PV,
* Speicher, Lüftung). Über alle Aufträge gerechnet läge die Quote im
* einstelligen Bereich und wäre wertlos, eine Badwartung war nie förderfähig.
*/
export function foerderQuote(auftraege: Auftrag[]): {
quote: number;
foerderfaehig: number;
bewilligt: number;
beantragt: number;
summe: number;
verteilung: ChartPunkt[];
} {
const foerderfaehig = auftraege.filter((a) => FOERDERFAEHIGE_KATEGORIEN.has(a.kategorie));
const bewilligt = foerderfaehig.filter((a) => a.foerderung === "bewilligt");
const beantragt = foerderfaehig.filter((a) => a.foerderung === "beantragt");
const mitFoerderung = bewilligt.length + beantragt.length;
return {
quote: foerderfaehig.length ? (mitFoerderung / foerderfaehig.length) * 100 : 0,
foerderfaehig: foerderfaehig.length,
bewilligt: bewilligt.length,
beantragt: beantragt.length,
summe: bewilligt.reduce((s, a) => s + a.foerderbetrag, 0),
verteilung: [
{ name: "Bewilligt", value: bewilligt.length },
{ name: "Beantragt", value: beantragt.length },
{ name: "Nicht beantragt", value: foerderfaehig.length - mitFoerderung },
],
};
}