Compare commits

...

2 Commits

Author SHA1 Message Date
Bastian Wagner
3d774a3455 overflow 2026-08-03 11:16:15 +02:00
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
2 changed files with 112 additions and 0 deletions

View File

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

View File

@@ -7,6 +7,7 @@
html { html {
height: 100%; height: 100%;
overflow: hidden;
@include mat.theme( @include mat.theme(
( (
color: ( color: (