Adds a second historical line to the existing cash-balance chart showing what the balance would be if all outstanding player dues were paid, reconstructed per month the same way the existing balance line is.
6.5 KiB
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
balanceHistorybereits für den Kassenstand selbst nutzt (siehegetOverviewStats-Kommentar zuteam.balanceals Anker). - Datenform:
theoreticalBalancewird als zusätzliches Feld direkt in jeden bestehendenbalanceHistory-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 überrelations: ['players', 'players.transactions', 'transactions']), gefiltert aufplayer.active— dieselbe Teilmenge, diegetOverviewfür die heutige „Offene Beiträge"-Kachel verwendet. - Für jeden aktiven Spieler: dessen
transactions(bereits geladen), ausgenommen Zeilen mitnote?.startsWith(DEACTIVATION_ADJUSTMENT_NOTE_PREFIX)(Import austeam-members.service.ts) — dieselbe Ausschlussregel wie inTeamMembersService.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 inTeamMembersService.recomputeBalance(Zeile ~127-133) undTransactionsService.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 bestehendendescendingMovements/futureSum-Muster fürbalanceHistory, 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:
balanceHistory: { month: string; balance: number; theoreticalBalance: number }[]monthlyFlowundtopOutstandingbleiben unverändert. - Gating unverändert: Ist
movements.length === 0(keine Kassenbewegung je), bleibtbalanceHistory: []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:BalanceHistoryPointumtheoreticalBalance: numbererweitern.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#4f8f46der Ist-Linie),fill: false.
balanceChartOptions:plugins.legend.displayvonfalseauftrue(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 bestehendengetOverviewStats-Testblocks):- Sanity-Check:
balanceHistory.at(-1).theoreticalBalance === team.balance + aktuelle Summe offener Beiträge(heutiger Wert, wie vongetOverviewberechnet). - Historische Rekonstruktion: Testfall mit einer Strafe (
fine) in einem früheren Monat, die erst im aktuellen Monat bezahlt wurde —theoreticalBalanceim 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 weiterhinbalanceHistory: [].
- Sanity-Check:
- Frontend (
overview.spec.ts): Erweiterung des bestehenden Chart-Daten-Tests um Assertion, dassbalanceChartData()zwei Datasets enthält und die zweite Serie austheoreticalBalancegespeist 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).