import { CurrencyPipe, DatePipe, registerLocaleData } from '@angular/common'; import localeDe from '@angular/common/locales/de'; import { Component, LOCALE_ID, computed, inject, signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ActivatedRoute, RouterLink } from '@angular/router'; import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; import { MatButtonModule } from '@angular/material/button'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import type { ChartData, ChartOptions } from 'chart.js'; import { ChartCanvas } from '../../../shared/chart-canvas/chart-canvas'; import { TeamStore } from '../../../core/team/team-store'; import { TransactionsApi } from '../../../core/team/transactions-api'; import { TeamStatsApi } from '../../../core/team/team-stats-api'; import { TeamActivity } from '../../../models/transaction.model'; import { TeamOverviewStats } from '../../../models/team-stats.model'; import { signedTransactionAmount } from '../../../models/transaction-amount'; import { of } from 'rxjs'; import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators'; registerLocaleData(localeDe); const BALANCE_COLOR = '#4f8f46'; const INCOME_COLOR = '#4f8f46'; const EXPENSE_COLOR = '#c1121f'; function formatMonthLabel(month: string): string { const [year, monthNumber] = month.split('-').map(Number); return new Intl.DateTimeFormat('de-DE', { month: 'short', year: '2-digit' }).format( new Date(year, monthNumber - 1, 1), ); } @Component({ selector: 'app-overview', imports: [ CurrencyPipe, DatePipe, MatButtonModule, MatCardModule, MatIconModule, MatProgressSpinnerModule, RouterLink, ChartCanvas, ], providers: [{ provide: LOCALE_ID, useValue: 'de-DE' }], templateUrl: './overview.html', styleUrl: './overview.scss', }) export class Overview { private readonly route = inject(ActivatedRoute); private readonly transactionsApi = inject(TransactionsApi); private readonly teamStatsApi = inject(TeamStatsApi); protected readonly team = inject(TeamStore).team; protected readonly activities = signal([]); protected readonly loadingActivities = signal(true); protected readonly stats = signal(null); protected readonly loadingStats = signal(true); protected readonly balanceHistory = computed(() => this.stats()?.balanceHistory ?? []); protected readonly monthlyFlow = computed(() => this.stats()?.monthlyFlow ?? []); protected readonly topOutstanding = computed(() => this.stats()?.topOutstanding ?? []); protected readonly balanceChartData = computed(() => { const points = this.balanceHistory(); return { labels: points.map((point) => formatMonthLabel(point.month)), datasets: [ { label: 'Kassenstand', data: points.map((point) => point.balance), borderColor: BALANCE_COLOR, backgroundColor: BALANCE_COLOR, tension: 0.3, fill: false, }, ], }; }); protected readonly flowChartData = computed(() => { const points = this.monthlyFlow(); return { labels: points.map((point) => formatMonthLabel(point.month)), datasets: [ { label: 'Einnahmen', data: points.map((point) => point.income), backgroundColor: INCOME_COLOR, }, { label: 'Ausgaben', data: points.map((point) => point.expense), backgroundColor: EXPENSE_COLOR, }, ], }; }); protected readonly topOutstandingChartData = computed(() => { const players = this.topOutstanding(); return { labels: players.map((player) => player.playerName), datasets: [ { label: 'Offener Betrag', data: players.map((player) => player.balance), backgroundColor: EXPENSE_COLOR, }, ], }; }); protected readonly balanceChartOptions: ChartOptions = { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, }; protected readonly flowChartOptions: ChartOptions = { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom' } }, }; protected readonly topOutstandingChartOptions: ChartOptions = { indexAxis: 'y', responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, }; constructor() { const parentRoute = this.route.parent; if (!parentRoute) { this.loadingActivities.set(false); this.loadingStats.set(false); return; } const teamId$ = parentRoute.paramMap.pipe( map((params) => Number(params.get('id'))), distinctUntilChanged(), ); teamId$ .pipe( tap((id) => { this.activities.set([]); this.loadingActivities.set(Number.isInteger(id) && id > 0); }), switchMap((id) => Number.isInteger(id) && id > 0 ? this.transactionsApi.loadTeamTransactions(id).pipe(catchError(() => of([]))) : of([]), ), takeUntilDestroyed(), ) .subscribe((activities) => { this.activities.set(activities.slice(0, 10)); this.loadingActivities.set(false); }); teamId$ .pipe( tap((id) => { this.stats.set(null); this.loadingStats.set(Number.isInteger(id) && id > 0); }), switchMap((id) => Number.isInteger(id) && id > 0 ? this.teamStatsApi.loadStats(id).pipe(catchError(() => of(null))) : of(null), ), takeUntilDestroyed(), ) .subscribe((stats) => { this.stats.set(stats); this.loadingStats.set(false); }); } protected activityIcon(activity: TeamActivity): string { return activity.isTeamWalletTransaction ? 'account_balance' : 'person'; } protected displayAmount(activity: TeamActivity): number { return signedTransactionAmount(activity.amount, activity.type); } }