Files
teamwallet/docs/superpowers/specs/2026-08-01-kasse-kpi-charts-design.md
Bastian Wagner b6f311b11b 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.
2026-08-01 19:05:00 +02:00

8.3 KiB

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:

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