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.
This commit is contained in:
@@ -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).
|
||||||
Reference in New Issue
Block a user