docs: add design spec for cashbox KPI charts on team overview
Brainstormed with the user: three charts on the existing overview page (balance history, monthly income/expense, top-10 outstanding players), Chart.js as dependency-free charting lib, new backend aggregation endpoint since none of the existing endpoints group transactions by time or category.
This commit is contained in:
145
docs/superpowers/specs/2026-08-01-kasse-kpi-charts-design.md
Normal file
145
docs/superpowers/specs/2026-08-01-kasse-kpi-charts-design.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# Kasse-KPIs als Graphen auf der Team-Übersicht
|
||||
|
||||
Status: approved
|
||||
Datum: 2026-08-01
|
||||
|
||||
## Kontext
|
||||
|
||||
Frontend: Angular 21 (`myteamwallet_frontend_modern`), Angular Material als Design-System,
|
||||
`LOCALE_ID: 'de-DE'`. Backend: NestJS (`myteamwallet_backend`, basierend auf
|
||||
`nestjs-boilerplate`) mit TypeORM-Entities.
|
||||
|
||||
Die bestehende Team-Übersicht (`features/team/overview/overview.ts` + `.html`) zeigt aktuell nur
|
||||
zwei Kennzahlen-Kacheln (Teamkasse-Saldo, offene Beiträge, aus `GET /teams/:id/overview` bzw.
|
||||
`teams.service.ts#getOverview`) sowie eine Liste der letzten 10 Aktivitäten
|
||||
(`TransactionsApi.loadTeamTransactions`). Es gibt weder eine Chart-Bibliothek im Frontend noch
|
||||
einen Backend-Endpoint, der Transaktionen zeitlich oder kategorisch aggregiert — `Transaction`
|
||||
und `TeamWalletTransaction` liefern nur flache Listen; `team.balance`/`player.balance` sind reine
|
||||
Laufsummen ohne historische Zwischenstände.
|
||||
|
||||
Datenmodell (relevant für Aggregation):
|
||||
|
||||
- `Transaction` (Spieler-Ebene, `transaction-type.enum.ts`): `payment` (id 0), `credit` (id 1),
|
||||
`fine` (11), `levy` (12), `fee` (13). Nur `payment` verändert laut
|
||||
`transaction.entity.ts#setBalance()` zusätzlich `team.balance` — Strafen/Beiträge (`fine`,
|
||||
`levy`, `fee`) erhöhen nur die Schuld des Spielers (`player.balance`), bis sie bezahlt werden.
|
||||
- `TeamWalletTransaction` (Team-Ebene, `team-wallet-transaction.enum.ts`): `credit` (1),
|
||||
`expense` (14) — verändern `team.balance` direkt.
|
||||
|
||||
Ziel: auf der Übersicht drei KPI-Graphen ergänzen, damit Trainer/Kassenwarte den Kassenverlauf
|
||||
auf einen Blick erfassen, ohne die volle Aktivitätsliste durchsuchen zu müssen.
|
||||
|
||||
## Entscheidungen aus dem Brainstorming
|
||||
|
||||
- **KPIs**: Kassenstand-Verlauf über Zeit, Einnahmen vs. Ausgaben pro Monat, offene Beiträge je
|
||||
Spieler (Top 10). Keine Kategorie-Verteilung (Strafen/Beiträge/Ausgaben-Anteile) in diesem Zug.
|
||||
- **Platzierung**: direkt auf der bestehenden Übersicht-Seite, kein neuer Tab/Bereich.
|
||||
- **Zeitraum**: feste laufende Saison, letzte 12 Monate — kein Zeitraum-Umschalter in diesem Zug.
|
||||
- **Chart-Bibliothek**: `chart.js` direkt (kein `ng2-charts`/`ngx-charts`-Wrapper), um
|
||||
Peer-Dependency-Risiken mit dem sehr neuen Angular 21 zu vermeiden — Chart.js hat keine
|
||||
Angular-Abhängigkeit.
|
||||
- **Einnahmen-Logik**: „Ist-Kasse" — nur tatsächliche Zahlungsbewegungen zählen als
|
||||
Einnahme/Ausgabe (Spieler-`payment` + Team-Wallet-`credit`/`expense`). Verhängte, aber noch
|
||||
nicht bezahlte `fine`/`levy`/`fee` zählen **nicht** mit — konsistent mit dem
|
||||
Kassenstand-Verlauf, der denselben Datenausschnitt nutzt.
|
||||
- **Offene-Beiträge-Chart**: nur Top 10 Schuldner (höchste negative `player.balance`, nur aktive
|
||||
Spieler), mit Link zur bestehenden Mitgliederverwaltung (`team/:id/members`) für die
|
||||
vollständige Liste.
|
||||
|
||||
## Architektur / Komponenten
|
||||
|
||||
### 1. Backend: neuer Aggregations-Endpoint
|
||||
|
||||
Neue Route `GET /teams/:id/overview/stats` in `teams.controller.ts`, Logik in
|
||||
`teams.service.ts` (neue Methode `getOverviewStats(teamId)`, analog zu `getOverview`).
|
||||
Antwortform:
|
||||
|
||||
```ts
|
||||
interface TeamOverviewStats {
|
||||
balanceHistory: { month: string /* 'YYYY-MM' */; balance: number }[]; // 12 Einträge
|
||||
monthlyFlow: { month: string; income: number; expense: number }[]; // 12 Einträge
|
||||
topOutstanding: { playerId: number; playerName: string; balance: number }[]; // max. 10
|
||||
}
|
||||
```
|
||||
|
||||
Berechnung:
|
||||
|
||||
- Relevante Rohdaten: alle `Transaction` vom Typ `payment` des Teams + alle
|
||||
`TeamWalletTransaction` des Teams, jeweils mit `date` und `amount`, aufsteigend sortiert.
|
||||
(Wiederverwendung der bestehenden Relationen `team.players.transactions` /
|
||||
`team.transactions`, wie in `getTeamTransactions` bereits geladen — Filterung auf `payment`
|
||||
ergänzen.)
|
||||
- `balanceHistory`: kumulative Summe der Rohdaten bilden, pro Kalendermonat der letzten 12 Monate
|
||||
den Stand am Monatsende übernehmen; Monate ohne Bewegung übernehmen den letzten bekannten
|
||||
Stand. Vorzeichen wie in den bestehenden `setBalance()`-Methoden: `amount` ist in der DB stets
|
||||
positiv gespeichert, `expense` (`TeamWalletTransaction`, `type.id` 14) wird beim Aufsummieren
|
||||
abgezogen, `payment`/`credit` addiert. Der letzte Wert der Reihe muss `team.balance`
|
||||
entsprechen (Sanity-Check im Unit-Test).
|
||||
- `monthlyFlow`: dieselben Rohdaten nach Monat gruppieren; `payment` und `credit` (positiver
|
||||
Betrag) fließen in `income`, `expense` in `expense` (als positive Summe ausgewiesen, nicht
|
||||
negativ).
|
||||
- `topOutstanding`: aktive Spieler (`player.active`) mit `balance < 0` laden (gleiche
|
||||
Player-Relation wie `getOverview`), nach `balance` aufsteigend (= höchste Schuld zuerst)
|
||||
sortieren, auf 10 begrenzen, `balance` als positiver `outstanding`-Betrag ausgeben.
|
||||
|
||||
Kein neues TypeORM-Entity, keine neue Tabelle — reine Ableitung aus bestehenden Daten zur
|
||||
Laufzeit (Datenvolumen pro Team ist klein genug, keine Materialisierung nötig).
|
||||
|
||||
### 2. Frontend: Chart-Integration
|
||||
|
||||
- Neue Dependency: `chart.js` (`npm install chart.js`, kein zusätzlicher Angular-Wrapper).
|
||||
- Neue wiederverwendbare Komponente `shared/chart-canvas/chart-canvas.ts` (+ `.html`/`.scss`):
|
||||
kapselt ein `<canvas>`-Element und den Chart.js-Instanz-Lifecycle. Inputs: `type` (`'line'` |
|
||||
`'bar'`), `data`, `options` (Chart.js-native Typen). Erstellt die `Chart`-Instanz in
|
||||
`afterNextRender`/`ngAfterViewInit`, aktualisiert sie über `effect()` bei Input-Änderungen,
|
||||
zerstört sie in `ngOnDestroy`. Wird von allen drei KPI-Charts mit unterschiedlicher Config
|
||||
genutzt — kein chart-spezifischer Code dupliziert sich.
|
||||
- Neuer `TeamStatsApi`-Service (`core/team/team-stats-api.ts`, analog zu
|
||||
`core/team/transactions-api.ts`) mit `loadStats(teamId): Observable<TeamOverviewStats>`, neues
|
||||
Model `TeamOverviewStats` in `models/`.
|
||||
|
||||
### 3. UI: `overview.ts` / `overview.html`
|
||||
|
||||
- `Overview`-Component bekommt ein zusätzliches `stats`-Signal + `loadingStats`-Signal, gefüllt
|
||||
über denselben `switchMap`-auf-Route-Param-Pattern wie `activities`
|
||||
(`teamStatsApi.loadStats(id).pipe(catchError(() => of(null)))`).
|
||||
- Neue Sektion zwischen Balance-Kacheln und Aktivitätsliste, drei `mat-card`s:
|
||||
1. Liniendiagramm „Kassenstand-Verlauf" (`balanceHistory`).
|
||||
2. Gruppiertes Balkendiagramm „Einnahmen & Ausgaben" (`monthlyFlow`, zwei Serien).
|
||||
3. Horizontales Balkendiagramm „Offene Beiträge (Top 10)" (`topOutstanding`), darunter ein
|
||||
Link/Button „Alle Spieler ansehen" → `routerLink` zu `members` innerhalb des Team-Kontexts.
|
||||
- Jede Chart-Karte hat einen eigenen Ladezustand (`mat-spinner`, wie bei der Aktivitätsliste) und
|
||||
einen Empty-State bei leeren Arrays (z. B. neues Team ohne Bewegungen) statt eines leeren
|
||||
Canvas.
|
||||
- Chart-Farben orientieren sich an der bestehenden `balance-card`/Material-Palette (Grün für
|
||||
positiv/Einnahmen, Rot-Ton für negativ/Ausgaben) — App hat aktuell nur ein Light-Theme
|
||||
(`color-scheme: light` in `styles.scss`), kein Dark-Mode-Handling nötig.
|
||||
|
||||
## Fehlerbehandlung
|
||||
|
||||
Fehler beim Laden der Stats führen zu einem stillen Empty-State pro Chart-Karte (kein globaler
|
||||
Fehlerblock, keine Snackbar) — konsistent mit dem bestehenden Umgang bei `activities`
|
||||
(`catchError(() => of([]))`). Der Rest der Übersicht-Seite (Balance-Kacheln, Aktivitätsliste)
|
||||
bleibt unabhängig vom Erfolg des Stats-Requests voll funktionsfähig.
|
||||
|
||||
## Testing
|
||||
|
||||
- Backend: neuer Jest-Unit-Test-Block für `getOverviewStats` in `teams.service.spec.ts` —
|
||||
prüft Monatsgruppierung, Ist-Kasse-Filterung (fine/levy/fee werden ignoriert), Top-10-Sortierung
|
||||
und den Sanity-Check `balanceHistory.at(-1).balance === team.balance`.
|
||||
- Backend: Controller-Test für die neue Route (Auth-Guard greift, Response-Form) in
|
||||
`teams.controller.spec.ts`, analog zu bestehenden Tests für `/overview`.
|
||||
- Frontend: Erweiterung von `overview.spec.ts` um Fälle mit gemocktem `TeamStatsApi`
|
||||
(Loading-, Empty- und Daten-Zustand pro Chart-Karte).
|
||||
- Manuelle Verifikation: Team mit realistischer Transaktionshistorie lokal aufrufen, alle drei
|
||||
Charts visuell prüfen (inkl. Team ohne jegliche Bewegungen → Empty-States statt Fehler).
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Zeitraum-Umschalter / freie Datumsauswahl für die Charts.
|
||||
- Kategorie-Verteilungs-Chart (Anteile Strafen/Beiträge/Ausgaben).
|
||||
- Dark-Mode-spezifisches Chart-Theming (App hat aktuell kein Dark-Theme).
|
||||
- Anzeige aller Spieler im Offene-Beiträge-Chart (nur Top 10 + Link auf bestehende
|
||||
Mitgliederverwaltung).
|
||||
- Persistierung/Materialisierung historischer Kassenstände (Berechnung erfolgt zur Laufzeit aus
|
||||
bestehenden Transaktionsdaten).
|
||||
Reference in New Issue
Block a user