# Theoretischer Kassenstand (Ist + offene Beiträge) im Kassenstand-Verlauf Status: approved Datum: 2026-08-03 ## Kontext Ergänzung zum bestehenden Kassenstand-Verlauf-Chart aus `docs/superpowers/specs/2026-08-01-kasse-kpi-charts-design.md`. Der Chart auf der Team-Übersicht (`features/team/overview/overview.ts` + `.html`) zeigt aktuell eine Linie „Kassenstand" über die letzten 12 Monate, gespeist aus `GET /teams/:id/overview/stats` bzw. `teams.service.ts#getOverviewStats`. Auf derselben Übersicht existiert bereits eine zweite Kennzahl „Offene Beiträge" (aus `teams.service.ts#getOverview`, Zeile 46-53): `-(Summe der `balance` aller aktiven Spieler)`. Sie zeigt nur den heutigen Wert, keinen Verlauf. Ziel: eine zweite Linie im bestehenden Chart, die pro Monat den theoretischen Kassenstand zeigt — also „was wäre in der Kasse, wenn alle offenen Beiträge bereits bezahlt worden wären" —, um Trainer/ Kassenwarte auf einen Blick erkennen zu lassen, wie stark der Ist-Stand vom Soll-Stand abweicht. ## Entscheidungen aus dem Brainstorming - **Historie statt Snapshot**: Die zweite Linie zeigt für jeden Monat die zu diesem Zeitpunkt tatsächlich offenen Beiträge, nicht den heutigen Wert konstant über alle 12 Punkte addiert. - **Näherung wie beim bestehenden Kassenstand-Verlauf**: Es wird mit der *heutigen* Menge aktiver Spieler gerechnet, kein historisches Tracking von Mitgliedschaft/Aktiv-Status. Das ist dieselbe Vereinfachung, die `balanceHistory` bereits für den Kassenstand selbst nutzt (siehe `getOverviewStats`-Kommentar zu `team.balance` als Anker). - **Datenform**: `theoreticalBalance` wird als zusätzliches Feld direkt in jeden bestehenden `balanceHistory`-Punkt eingebettet (`{ month, balance, theoreticalBalance }`), kein separates Array — additive, nicht-brechende Erweiterung der bestehenden Response. ## Architektur / Komponenten ### 1. Backend: `teams.service.ts#getOverviewStats` Neue private Hilfsberechnung, analog zur bestehenden Rückwärts-Rekonstruktion von `balanceHistory` (Zeile ~290-320), aber auf Spieler-Ebene statt Team-Ebene: - Datenquelle: `team.players` (bereits geladen über `relations: ['players', 'players.transactions', 'transactions']`), gefiltert auf `player.active` — dieselbe Teilmenge, die `getOverview` für die heutige „Offene Beiträge"-Kachel verwendet. - Für jeden aktiven Spieler: dessen `transactions` (bereits geladen), **ausgenommen** Zeilen mit `note?.startsWith(DEACTIVATION_ADJUSTMENT_NOTE_PREFIX)` (Import aus `team-members.service.ts`) — dieselbe Ausschlussregel wie in `TeamMembersService.recomputeBalance`, damit synthetische Ausgleichsbuchungen die Historie nicht verfälschen. - Vorzeichen je Buchung: `type.id > 10` (Strafe/Umlage/Gebühr, IDs 11-13) mindert den Spieler-Saldo, alle anderen Typen (`payment`, `credit`) erhöhen ihn — identische Regel wie in `TeamMembersService.recomputeBalance` (Zeile ~127-133) und `TransactionsService.reverse()` (`type.id > 10`-Check). - Rekonstruktion: ausgehend von `player.balance` (aktueller, autoritativer Wert) rückwärts durch die nach Datum absteigend sortierten Buchungen laufen und pro Monat der letzten 12 Monate den rekonstruierten Saldo am Monatsende ermitteln — strukturell identisch zum bestehenden `descendingMovements`/`futureSum`-Muster für `balanceHistory`, nur pro Spieler statt einmal fürs Team. - Pro Monat: `outstandingAtMonth = -Σ(rekonstruierter Saldo aktiver Spieler)`, `theoreticalBalance = balanceHistory[monat].balance + outstandingAtMonth`. - Rückgabeform ändert sich zu: ```ts balanceHistory: { month: string; balance: number; theoreticalBalance: number }[] ``` `monthlyFlow` und `topOutstanding` bleiben unverändert. - Gating unverändert: Ist `movements.length === 0` (keine Kassenbewegung je), bleibt `balanceHistory: []` wie heute — ein Team mit ausschließlich unbezahlten Strafen, aber ganz ohne Zahlungsbewegung, zeigt weiterhin keinen Chart (Out of Scope, siehe unten). ### 2. Frontend: `overview.ts` / Chart-Konfiguration - `models/team-stats.model.ts`: `BalanceHistoryPoint` um `theoreticalBalance: number` erweitern. - `balanceChartData` (computed) bekommt eine zweite Dataset-Eintrag: - Label: „Theoretisch (inkl. offene Beiträge)" - `data: points.map((p) => p.theoreticalBalance)` - Gestrichelt (`borderDash: [6, 4]`), eigene Farbe `#1d70b8` (Blauton, klar unterscheidbar vom Grün `#4f8f46` der Ist-Linie), `fill: false`. - `balanceChartOptions`: `plugins.legend.display` von `false` auf `true` (bzw. `position: 'bottom'` wie beim Flow-Chart), da jetzt zwei Linien unterschieden werden müssen. - Keine Änderung an `ChartCanvas` (shared component) nötig — reine Config-/Daten-Änderung. ## Fehlerbehandlung Unverändert zum bestehenden Muster: Fehler beim Laden der Stats führen zum bestehenden stillen Empty-State der Chart-Karte. Kein neuer Fehlerfall durch diese Erweiterung. ## Testing - Backend (`teams.service.spec.ts`, Erweiterung des bestehenden `getOverviewStats`-Testblocks): - Sanity-Check: `balanceHistory.at(-1).theoreticalBalance === team.balance + aktuelle Summe offener Beiträge` (heutiger Wert, wie von `getOverview` berechnet). - Historische Rekonstruktion: Testfall mit einer Strafe (`fine`) in einem früheren Monat, die erst im aktuellen Monat bezahlt wurde — `theoreticalBalance` im früheren Monat muss die damals offene Strafe enthalten, `balance` (Ist) nicht. - Deaktivierungs-Ausgleichsbuchungen werden aus der Rekonstruktion ausgeschlossen (Testfall mit einem zwischenzeitlich deaktivierten und wieder aktivierten Spieler). - Leerfall (`movements.length === 0`) liefert weiterhin `balanceHistory: []`. - Frontend (`overview.spec.ts`): Erweiterung des bestehenden Chart-Daten-Tests um Assertion, dass `balanceChartData()` zwei Datasets enthält und die zweite Serie aus `theoreticalBalance` gespeist wird. - Manuelle Verifikation: Team mit einer unbezahlten Strafe/Umlage lokal aufrufen, prüfen dass die theoretische Linie sichtbar über der Ist-Linie liegt und bei vollständiger Bezahlung beide Linien zusammenlaufen. ## Out of Scope - Historisches Tracking von Mitgliedschaft/Aktiv-Status (Näherung mit heutiger aktiver Spieler-Menge, siehe oben). - Teams mit ausschließlich unbezahlten Strafen/Umlagen/Gebühren, aber ganz ohne Kassenbewegung — zeigen weiterhin keinen Chart (bestehende Einschränkung aus dem Basis-Feature, nicht neu eingeführt). - Zeitraum-Umschalter (weiterhin feste letzte 12 Monate, wie im Basis-Feature festgelegt).