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.
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). Nurpaymentverändert lauttransaction.entity.ts#setBalance()zusätzlichteam.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ändernteam.balancedirekt.
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.jsdirekt (keinng2-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 bezahltefine/levy/feezä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
Transactionvom Typpaymentdes Teams + alleTeamWalletTransactiondes Teams, jeweils mitdateundamount, aufsteigend sortiert. (Wiederverwendung der bestehenden Relationenteam.players.transactions/team.transactions, wie ingetTeamTransactionsbereits geladen — Filterung aufpaymentergä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 bestehendensetBalance()-Methoden:amountist in der DB stets positiv gespeichert,expense(TeamWalletTransaction,type.id14) wird beim Aufsummieren abgezogen,payment/creditaddiert. Der letzte Wert der Reihe mussteam.balanceentsprechen (Sanity-Check im Unit-Test).monthlyFlow: dieselben Rohdaten nach Monat gruppieren;paymentundcredit(positiver Betrag) fließen inincome,expenseinexpense(als positive Summe ausgewiesen, nicht negativ).topOutstanding: aktive Spieler (player.active) mitbalance < 0laden (gleiche Player-Relation wiegetOverview), nachbalanceaufsteigend (= höchste Schuld zuerst) sortieren, auf 10 begrenzen,balanceals positiveroutstanding-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 dieChart-Instanz inafterNextRender/ngAfterViewInit, aktualisiert sie übereffect()bei Input-Änderungen, zerstört sie inngOnDestroy. 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 zucore/team/transactions-api.ts) mitloadStats(teamId): Observable<TeamOverviewStats>, neues ModelTeamOverviewStatsinmodels/.
3. UI: overview.ts / overview.html
Overview-Component bekommt ein zusätzlichesstats-Signal +loadingStats-Signal, gefüllt über denselbenswitchMap-auf-Route-Param-Pattern wieactivities(teamStatsApi.loadStats(id).pipe(catchError(() => of(null)))).- Neue Sektion zwischen Balance-Kacheln und Aktivitätsliste, drei
mat-cards:- Liniendiagramm „Kassenstand-Verlauf" (
balanceHistory). - Gruppiertes Balkendiagramm „Einnahmen & Ausgaben" (
monthlyFlow, zwei Serien). - Horizontales Balkendiagramm „Offene Beiträge (Top 10)" (
topOutstanding), darunter ein Link/Button „Alle Spieler ansehen" →routerLinkzumembersinnerhalb des Team-Kontexts.
- Liniendiagramm „Kassenstand-Verlauf" (
- 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: lightinstyles.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
getOverviewStatsinteams.service.spec.ts— prüft Monatsgruppierung, Ist-Kasse-Filterung (fine/levy/fee werden ignoriert), Top-10-Sortierung und den Sanity-CheckbalanceHistory.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.tsum Fälle mit gemocktemTeamStatsApi(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).