|
|
@ -5,19 +5,24 @@ import { UserService } from '@ghostfolio/client/services/user/user.service'; |
|
|
import { |
|
|
import { |
|
|
DEFAULT_CURRENCY, |
|
|
DEFAULT_CURRENCY, |
|
|
DEFAULT_DATE_RANGE, |
|
|
DEFAULT_DATE_RANGE, |
|
|
|
|
|
DEFAULT_OVERVIEW_CHART_MODE, |
|
|
NUMERICAL_PRECISION_THRESHOLD_6_FIGURES |
|
|
NUMERICAL_PRECISION_THRESHOLD_6_FIGURES |
|
|
} from '@ghostfolio/common/config'; |
|
|
} from '@ghostfolio/common/config'; |
|
|
import { |
|
|
import { |
|
|
AssetProfileIdentifier, |
|
|
AssetProfileIdentifier, |
|
|
|
|
|
HistoricalDataItem, |
|
|
LineChartItem, |
|
|
LineChartItem, |
|
|
PortfolioPerformance, |
|
|
PortfolioPerformance, |
|
|
User |
|
|
User, |
|
|
|
|
|
UserSettings |
|
|
} from '@ghostfolio/common/interfaces'; |
|
|
} from '@ghostfolio/common/interfaces'; |
|
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; |
|
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; |
|
|
import { internalRoutes } from '@ghostfolio/common/routes/routes'; |
|
|
import { internalRoutes } from '@ghostfolio/common/routes/routes'; |
|
|
import { hasScope, scopes } from '@ghostfolio/common/scopes'; |
|
|
import { hasScope, scopes } from '@ghostfolio/common/scopes'; |
|
|
|
|
|
import { OverviewChartMode, ToggleOption } from '@ghostfolio/common/types'; |
|
|
import { GfLineChartComponent } from '@ghostfolio/ui/line-chart'; |
|
|
import { GfLineChartComponent } from '@ghostfolio/ui/line-chart'; |
|
|
import { DataService } from '@ghostfolio/ui/services'; |
|
|
import { DataService } from '@ghostfolio/ui/services'; |
|
|
|
|
|
import { GfToggleComponent } from '@ghostfolio/ui/toggle'; |
|
|
|
|
|
|
|
|
import { |
|
|
import { |
|
|
ChangeDetectionStrategy, |
|
|
ChangeDetectionStrategy, |
|
|
@ -31,6 +36,7 @@ import { |
|
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
|
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
|
|
import { MatButtonModule } from '@angular/material/button'; |
|
|
import { MatButtonModule } from '@angular/material/button'; |
|
|
import { RouterModule } from '@angular/router'; |
|
|
import { RouterModule } from '@angular/router'; |
|
|
|
|
|
import { isEqual, isNumber, omit } from 'lodash'; |
|
|
import { DeviceDetectorService } from 'ngx-device-detector'; |
|
|
import { DeviceDetectorService } from 'ngx-device-detector'; |
|
|
|
|
|
|
|
|
@Component({ |
|
|
@Component({ |
|
|
@ -38,6 +44,7 @@ import { DeviceDetectorService } from 'ngx-device-detector'; |
|
|
imports: [ |
|
|
imports: [ |
|
|
GfLineChartComponent, |
|
|
GfLineChartComponent, |
|
|
GfPortfolioPerformanceComponent, |
|
|
GfPortfolioPerformanceComponent, |
|
|
|
|
|
GfToggleComponent, |
|
|
MatButtonModule, |
|
|
MatButtonModule, |
|
|
RouterModule |
|
|
RouterModule |
|
|
], |
|
|
], |
|
|
@ -46,13 +53,26 @@ import { DeviceDetectorService } from 'ngx-device-detector'; |
|
|
templateUrl: './home-overview.html' |
|
|
templateUrl: './home-overview.html' |
|
|
}) |
|
|
}) |
|
|
export class GfHomeOverviewComponent implements OnInit { |
|
|
export class GfHomeOverviewComponent implements OnInit { |
|
|
|
|
|
protected readonly chart = signal<HistoricalDataItem[] | null>(null); |
|
|
protected readonly errors = signal<AssetProfileIdentifier[]>([]); |
|
|
protected readonly errors = signal<AssetProfileIdentifier[]>([]); |
|
|
protected readonly hasImpersonationId = signal(false); |
|
|
protected readonly hasImpersonationId = signal(false); |
|
|
protected readonly historicalDataItems = signal<LineChartItem[] | null>(null); |
|
|
|
|
|
protected readonly isLoadingPerformance = signal(true); |
|
|
protected readonly isLoadingPerformance = signal(true); |
|
|
|
|
|
protected readonly netWorthLabel = $localize`Net Worth`; |
|
|
|
|
|
protected readonly overviewChartMode = signal<OverviewChartMode>( |
|
|
|
|
|
DEFAULT_OVERVIEW_CHART_MODE |
|
|
|
|
|
); |
|
|
|
|
|
protected readonly overviewChartModeOptions: ToggleOption[] = [ |
|
|
|
|
|
{ |
|
|
|
|
|
label: $localize`Investments`, |
|
|
|
|
|
value: 'PERFORMANCE' satisfies OverviewChartMode |
|
|
|
|
|
}, |
|
|
|
|
|
{ |
|
|
|
|
|
label: $localize`Net Worth`, |
|
|
|
|
|
value: 'NET_WORTH' satisfies OverviewChartMode |
|
|
|
|
|
} |
|
|
|
|
|
]; |
|
|
protected readonly performance = signal<PortfolioPerformance | null>(null); |
|
|
protected readonly performance = signal<PortfolioPerformance | null>(null); |
|
|
protected readonly performanceLabel = $localize`Performance`; |
|
|
protected readonly performanceLabel = $localize`Performance`; |
|
|
protected readonly precision = signal(2); |
|
|
|
|
|
protected readonly user = signal<User | null>(null); |
|
|
protected readonly user = signal<User | null>(null); |
|
|
|
|
|
|
|
|
protected readonly routerLinkAccounts = internalRoutes.accounts.routerLink; |
|
|
protected readonly routerLinkAccounts = internalRoutes.accounts.routerLink; |
|
|
@ -87,6 +107,87 @@ export class GfHomeOverviewComponent implements OnInit { |
|
|
: '%'; |
|
|
: '%'; |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
protected readonly chartCurrency = computed(() => { |
|
|
|
|
|
return this.overviewChartMode() === 'NET_WORTH' |
|
|
|
|
|
? (this.user()?.settings?.baseCurrency ?? DEFAULT_CURRENCY) |
|
|
|
|
|
: undefined; |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
protected readonly chartLabel = computed(() => { |
|
|
|
|
|
return this.overviewChartMode() === 'NET_WORTH' |
|
|
|
|
|
? this.netWorthLabel |
|
|
|
|
|
: this.performanceLabel; |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
protected readonly chartUnit = computed(() => { |
|
|
|
|
|
return this.chartCurrency() ? undefined : '%'; |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
protected readonly precision = computed(() => { |
|
|
|
|
|
const currentValue = |
|
|
|
|
|
(this.overviewChartMode() === 'NET_WORTH' |
|
|
|
|
|
? this.performance()?.currentNetWorth |
|
|
|
|
|
: this.performance()?.currentValueInBaseCurrency) ?? 0; |
|
|
|
|
|
|
|
|
|
|
|
return this.deviceType() === 'mobile' && |
|
|
|
|
|
currentValue >= NUMERICAL_PRECISION_THRESHOLD_6_FIGURES |
|
|
|
|
|
? 0 |
|
|
|
|
|
: 2; |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
protected readonly netWorthChange = computed(() => { |
|
|
|
|
|
const chart = this.chart(); |
|
|
|
|
|
|
|
|
|
|
|
if (!chart?.length) { |
|
|
|
|
|
return undefined; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const netWorthEnd = chart[chart.length - 1].netWorth; |
|
|
|
|
|
const netWorthStart = chart[0].netWorth; |
|
|
|
|
|
|
|
|
|
|
|
return isNumber(netWorthStart) && isNumber(netWorthEnd) |
|
|
|
|
|
? netWorthEnd - netWorthStart |
|
|
|
|
|
: undefined; |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
protected readonly netWorthChangeInPercentage = computed(() => { |
|
|
|
|
|
const netWorthChange = this.netWorthChange(); |
|
|
|
|
|
const netWorthStart = this.chart()?.[0]?.netWorth; |
|
|
|
|
|
|
|
|
|
|
|
// A date range starting before the first activity has a net worth of 0,
|
|
|
|
|
|
// for which a relative change is not defined
|
|
|
|
|
|
return isNumber(netWorthChange) && netWorthStart |
|
|
|
|
|
? netWorthChange / netWorthStart |
|
|
|
|
|
: undefined; |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
protected readonly historicalDataItems = computed<LineChartItem[] | null>( |
|
|
|
|
|
() => { |
|
|
|
|
|
const chart = this.chart(); |
|
|
|
|
|
|
|
|
|
|
|
if (!chart) { |
|
|
|
|
|
return null; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (this.overviewChartMode() === 'NET_WORTH') { |
|
|
|
|
|
return chart.map(({ date, netWorth }) => { |
|
|
|
|
|
return { date, value: netWorth ?? 0 }; |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return chart.map( |
|
|
|
|
|
({ date, netPerformanceInPercentageWithCurrencyEffect }) => { |
|
|
|
|
|
return { |
|
|
|
|
|
date, |
|
|
|
|
|
value: (netPerformanceInPercentageWithCurrencyEffect ?? 0) * 100 |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
private previousUserSettings: Omit<UserSettings, 'overviewChartMode'>; |
|
|
|
|
|
|
|
|
private readonly dataService = inject(DataService); |
|
|
private readonly dataService = inject(DataService); |
|
|
private readonly destroyRef = inject(DestroyRef); |
|
|
private readonly destroyRef = inject(DestroyRef); |
|
|
private readonly deviceDetectorService = inject(DeviceDetectorService); |
|
|
private readonly deviceDetectorService = inject(DeviceDetectorService); |
|
|
@ -102,7 +203,25 @@ export class GfHomeOverviewComponent implements OnInit { |
|
|
.subscribe((state) => { |
|
|
.subscribe((state) => { |
|
|
if (state?.user) { |
|
|
if (state?.user) { |
|
|
this.user.set(state.user); |
|
|
this.user.set(state.user); |
|
|
this.update(); |
|
|
|
|
|
|
|
|
// The net worth is not exposed in the restricted view and in the
|
|
|
|
|
|
// ZEN mode, where the chart mode is therefore not offered
|
|
|
|
|
|
this.overviewChartMode.set( |
|
|
|
|
|
this.showDetails() |
|
|
|
|
|
? (state.user.settings?.overviewChartMode ?? |
|
|
|
|
|
DEFAULT_OVERVIEW_CHART_MODE) |
|
|
|
|
|
: DEFAULT_OVERVIEW_CHART_MODE |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
// The chart mode is applied on the client, so changing it alone must
|
|
|
|
|
|
// not refetch the portfolio performance
|
|
|
|
|
|
const userSettings = omit(state.user.settings, 'overviewChartMode'); |
|
|
|
|
|
|
|
|
|
|
|
if (!isEqual(userSettings, this.previousUserSettings)) { |
|
|
|
|
|
this.previousUserSettings = userSettings; |
|
|
|
|
|
|
|
|
|
|
|
this.update(); |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
}); |
|
|
}); |
|
|
} |
|
|
} |
|
|
@ -122,8 +241,22 @@ export class GfHomeOverviewComponent implements OnInit { |
|
|
}); |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
protected onChangeOverviewChartMode(overviewChartMode: OverviewChartMode) { |
|
|
|
|
|
this.overviewChartMode.set(overviewChartMode); |
|
|
|
|
|
|
|
|
|
|
|
this.dataService |
|
|
|
|
|
.putUserSetting({ overviewChartMode }) |
|
|
|
|
|
.pipe(takeUntilDestroyed(this.destroyRef)) |
|
|
|
|
|
.subscribe(() => { |
|
|
|
|
|
this.userService |
|
|
|
|
|
.get(true) |
|
|
|
|
|
.pipe(takeUntilDestroyed(this.destroyRef)) |
|
|
|
|
|
.subscribe(); |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
private update() { |
|
|
private update() { |
|
|
this.historicalDataItems.set(null); |
|
|
this.chart.set(null); |
|
|
this.isLoadingPerformance.set(true); |
|
|
this.isLoadingPerformance.set(true); |
|
|
|
|
|
|
|
|
this.dataService |
|
|
this.dataService |
|
|
@ -135,26 +268,7 @@ export class GfHomeOverviewComponent implements OnInit { |
|
|
this.errors.set(errors ?? []); |
|
|
this.errors.set(errors ?? []); |
|
|
this.performance.set(performance); |
|
|
this.performance.set(performance); |
|
|
|
|
|
|
|
|
this.historicalDataItems.set( |
|
|
this.chart.set(chart ?? null); |
|
|
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); |
|
|
this.isLoadingPerformance.set(false); |
|
|
}); |
|
|
}); |
|
|
|