Recreated from the main checkout, where these were committed to local master but not yet pushed and thus missing from this fresh worktree.
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).