Files
teamwallet/docs/superpowers/specs/2026-08-03-theoretischer-kassenstand-design.md
Bastian Wagner fd850f372f docs: add design spec for theoretical cash balance line in overview chart
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.
2026-08-03 10:21:06 +02:00

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