import { GfPortfolioPerformanceComponent } from '@ghostfolio/client/components/portfolio-performance/portfolio-performance.component'; import { GfPortfolioPnlCalendarComponent } from '@ghostfolio/client/components/portfolio-pnl-calendar/portfolio-pnl-calendar.component'; import { LayoutService } from '@ghostfolio/client/core/layout.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { DEFAULT_CURRENCY, DEFAULT_DATE_RANGE, DEFAULT_LOCALE, NUMERICAL_PRECISION_THRESHOLD_6_FIGURES } from '@ghostfolio/common/config'; import { isCashPosition } from '@ghostfolio/common/helper'; import { AssetProfileIdentifier, HistoricalDataItem, LineChartItem, PortfolioPerformance, PortfolioPosition, User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { hasScope, scopes } from '@ghostfolio/common/scopes'; import { GfHoldingsTableComponent } from '@ghostfolio/ui/holdings-table'; import { GfLineChartComponent } from '@ghostfolio/ui/line-chart'; import { DataService } from '@ghostfolio/ui/services'; import { ChangeDetectionStrategy, Component, computed, DestroyRef, inject, OnInit, signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { Router, RouterModule } from '@angular/router'; import { DeviceDetectorService } from 'ngx-device-detector'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, imports: [ GfHoldingsTableComponent, GfLineChartComponent, GfPortfolioPnlCalendarComponent, GfPortfolioPerformanceComponent, MatButtonModule, MatCardModule, RouterModule ], selector: 'gf-home-overview', styleUrls: ['./home-overview.scss'], templateUrl: './home-overview.html' }) export class GfHomeOverviewComponent implements OnInit { protected readonly errors = signal([]); protected readonly hasImpersonationId = signal(false); protected readonly historicalDataItems = signal(null); protected readonly pnlCalendarChart = signal( null ); protected readonly holdings = signal( undefined ); protected readonly isLoadingPerformance = signal(true); protected readonly baseCurrency = computed(() => { return this.user()?.settings?.baseCurrency ?? DEFAULT_CURRENCY; }); protected readonly locale = computed(() => { return this.user()?.settings?.locale ?? DEFAULT_LOCALE; }); protected readonly performance = signal(null); protected readonly performanceLabel = $localize`Performance`; protected readonly precision = signal(2); protected readonly user = signal(null); protected readonly routerLinkAccounts = internalRoutes.accounts.routerLink; protected readonly routerLinkPortfolio = internalRoutes.portfolio.routerLink; protected readonly routerLinkPortfolioActivities = internalRoutes.portfolio.subRoutes.activities.routerLink; protected readonly routerLinkPortfolioActivitiesCreate = internalRoutes.portfolio.subRoutes.activities.subRoutes.create.routerLink; protected readonly deviceType = computed( () => this.deviceDetectorService.deviceInfo().deviceType ); protected readonly hasPermissionToCreateActivity = computed(() => { return ( hasPermission(this.user()?.permissions, permissions.createActivity) && hasScope(this.user()?.scopes, scopes.activityCreate) ); }); protected readonly showDetails = computed(() => { const user = this.user(); return user ? !user?.settings?.isRestrictedView && user?.settings?.viewMode !== 'ZEN' : false; }); protected readonly showExperimentalHoldings = computed(() => { const user = this.user(); return ( user?.settings?.isExperimentalFeatures === true && user?.settings?.viewMode !== 'ZEN' ); }); protected readonly unit = computed(() => { if (!this.showDetails()) { return '%'; } return this.deviceType() === 'mobile' ? undefined : (this.user()?.settings?.baseCurrency ?? DEFAULT_CURRENCY); }); private readonly dataService = inject(DataService); private readonly destroyRef = inject(DestroyRef); private readonly deviceDetectorService = inject(DeviceDetectorService); private readonly impersonationStorageService = inject( ImpersonationStorageService ); private readonly layoutService = inject(LayoutService); private readonly router = inject(Router); private readonly userService = inject(UserService); public constructor() { this.userService.stateChanged .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((state) => { if (state?.user) { this.user.set(state.user); this.update(); } }); } public ngOnInit() { this.impersonationStorageService .onChangeHasImpersonation() .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((impersonationId) => { this.hasImpersonationId.set(!!impersonationId); }); this.layoutService.shouldReloadContent$ .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { this.update(); }); } protected onHoldingClicked({ dataSource, symbol }: AssetProfileIdentifier) { if (dataSource && symbol) { void this.router.navigate([], { queryParams: { dataSource, symbol, holdingDetailDialog: true } }); } } private update() { const userSettings = this.user()?.settings; this.historicalDataItems.set(null); this.pnlCalendarChart.set(null); this.isLoadingPerformance.set(true); if (this.showExperimentalHoldings()) { this.holdings.set(undefined); this.dataService .fetchPortfolioHoldings({ filters: [{ id: 'ACTIVE', type: 'HOLDING_TYPE' }], range: userSettings?.dateRange }) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(({ holdings }) => { const baseCurrency = userSettings?.baseCurrency ?? DEFAULT_CURRENCY; this.holdings.set( holdings.filter(({ assetProfile }) => { return !( isCashPosition(assetProfile) && assetProfile.currency === baseCurrency && assetProfile.symbol === baseCurrency ); }) ); }); } this.dataService .fetchPortfolioPerformance({ range: userSettings?.dateRange ?? DEFAULT_DATE_RANGE }) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(({ chart, errors, performance }) => { this.errors.set(errors ?? []); this.performance.set(performance); // Reuse the overview request whenever it already covers all history. if ((userSettings?.dateRange ?? DEFAULT_DATE_RANGE) === 'max') { this.pnlCalendarChart.set(chart ?? []); } this.historicalDataItems.set( chart?.map( ({ date, netPerformanceInPercentageWithCurrencyEffect }) => { return { date, value: (netPerformanceInPercentageWithCurrencyEffect ?? 0) * 100 }; } ) ?? null ); this.precision.set(2); if ( this.deviceType() === 'mobile' && performance.currentValueInBaseCurrency >= NUMERICAL_PRECISION_THRESHOLD_6_FIGURES ) { this.precision.set(0); } this.isLoadingPerformance.set(false); }); if ((userSettings?.dateRange ?? DEFAULT_DATE_RANGE) !== 'max') { this.dataService .fetchPortfolioPerformance({ range: 'max' }) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: ({ chart }) => { this.pnlCalendarChart.set(chart ?? []); }, error: () => { this.pnlCalendarChart.set([]); } }); } } }