# 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).
