diff --git a/docs/superpowers/specs/2026-08-03-theoretischer-kassenstand-design.md b/docs/superpowers/specs/2026-08-03-theoretischer-kassenstand-design.md new file mode 100644 index 0000000..f49a682 --- /dev/null +++ b/docs/superpowers/specs/2026-08-03-theoretischer-kassenstand-design.md @@ -0,0 +1,111 @@ +# 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).