You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

255 lines
8.2 KiB

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<AssetProfileIdentifier[]>([]);
protected readonly hasImpersonationId = signal(false);
protected readonly historicalDataItems = signal<LineChartItem[] | null>(null);
protected readonly pnlCalendarChart = signal<HistoricalDataItem[] | null>(
null
);
protected readonly holdings = signal<PortfolioPosition[] | undefined>(
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<PortfolioPerformance | null>(null);
protected readonly performanceLabel = $localize`Performance`;
protected readonly precision = signal(2);
protected readonly user = signal<User | null>(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([]);
}
});
}
}
}