Browse Source

Task/improve type safety in analysis page component (#6965)

* fix(client): resolve errors in analysis page component

* feat(client): replace constructor based DI with inject functions

* feat(client): convert to view child signal

* feat(client): enforce encapsulation

* feat(client): enforce readability

* feat(client): replace deprecated getDeviceInfo

* feat(client): convert mode to signal
pull/6964/head^2
Kenrick Tandrian 8 hours ago
committed by GitHub
parent
commit
60be4de9a5
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 153
      apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
  2. 8
      apps/client/src/app/pages/portfolio/analysis/analysis-page.html

153
apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts

@ -2,7 +2,10 @@ import { GfBenchmarkComparatorComponent } from '@ghostfolio/client/components/be
import { GfInvestmentChartComponent } from '@ghostfolio/client/components/investment-chart/investment-chart.component'; import { GfInvestmentChartComponent } from '@ghostfolio/client/components/investment-chart/investment-chart.component';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { NUMERICAL_PRECISION_THRESHOLD_6_FIGURES } from '@ghostfolio/common/config'; import {
DEFAULT_DATE_RANGE,
NUMERICAL_PRECISION_THRESHOLD_6_FIGURES
} from '@ghostfolio/common/config';
import { import {
HistoricalDataItem, HistoricalDataItem,
InvestmentItem, InvestmentItem,
@ -24,9 +27,12 @@ import { Clipboard } from '@angular/cdk/clipboard';
import { import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
computed,
DestroyRef, DestroyRef,
inject,
OnInit, OnInit,
ViewChild signal,
viewChild
} from '@angular/core'; } from '@angular/core';
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';
@ -64,53 +70,57 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
templateUrl: './analysis-page.html' templateUrl: './analysis-page.html'
}) })
export class GfAnalysisPageComponent implements OnInit { export class GfAnalysisPageComponent implements OnInit {
@ViewChild(MatMenuTrigger) actionsMenuButton!: MatMenuTrigger; protected benchmark?: Partial<SymbolProfile>;
protected benchmarkDataItems: HistoricalDataItem[] = [];
public benchmark: Partial<SymbolProfile>; protected readonly benchmarks: Partial<SymbolProfile>[];
public benchmarkDataItems: HistoricalDataItem[] = []; protected bottom3: PortfolioPosition[];
public benchmarks: Partial<SymbolProfile>[]; protected dividendsByGroup: InvestmentItem[];
public bottom3: PortfolioPosition[]; protected readonly dividendTimelineDataLabel = $localize`Dividend`;
public deviceType: string; protected hasImpersonationId: boolean;
public dividendsByGroup: InvestmentItem[]; protected hasPermissionToReadAiPrompt: boolean;
public dividendTimelineDataLabel = $localize`Dividend`; protected investments: InvestmentItem[];
public firstOrderDate: Date; protected readonly investmentTimelineDataLabel = $localize`Investment`;
public hasImpersonationId: boolean; protected investmentsByGroup: InvestmentItem[];
public hasPermissionToReadAiPrompt: boolean; protected isLoadingAnalysisPrompt: boolean;
public investments: InvestmentItem[]; protected isLoadingBenchmarkComparator: boolean;
public investmentTimelineDataLabel = $localize`Investment`; protected isLoadingDividendTimelineChart: boolean;
public investmentsByGroup: InvestmentItem[]; protected isLoadingInvestmentChart: boolean;
public isLoadingAnalysisPrompt: boolean; protected isLoadingInvestmentTimelineChart: boolean;
public isLoadingBenchmarkComparator: boolean; protected isLoadingPortfolioPrompt: boolean;
public isLoadingDividendTimelineChart: boolean; protected readonly mode = signal<GroupBy>('month');
public isLoadingInvestmentChart: boolean; protected readonly modeOptions: ToggleOption[] = [
public isLoadingInvestmentTimelineChart: boolean;
public isLoadingPortfolioPrompt: boolean;
public mode: GroupBy = 'month';
public modeOptions: ToggleOption[] = [
{ label: $localize`Monthly`, value: 'month' }, { label: $localize`Monthly`, value: 'month' },
{ label: $localize`Yearly`, value: 'year' } { label: $localize`Yearly`, value: 'year' }
]; ];
public performance: PortfolioPerformance; protected performance: PortfolioPerformance;
public performanceDataItems: HistoricalDataItem[]; protected performanceDataItems: HistoricalDataItem[];
public performanceDataItemsInPercentage: HistoricalDataItem[]; protected performanceDataItemsInPercentage: HistoricalDataItem[];
public portfolioEvolutionDataLabel = $localize`Investment`; protected readonly portfolioEvolutionDataLabel = $localize`Investment`;
public precision = 2; protected precision = 2;
public streaks: PortfolioInvestmentsResponse['streaks']; protected streaks: PortfolioInvestmentsResponse['streaks'];
public top3: PortfolioPosition[]; protected top3: PortfolioPosition[];
public unitCurrentStreak: string; protected unitCurrentStreak: string;
public unitLongestStreak: string; protected unitLongestStreak: string;
public user: User; protected user: User;
public constructor( private readonly actionsMenuButton = viewChild.required(MatMenuTrigger);
private changeDetectorRef: ChangeDetectorRef, private readonly deviceType = computed(
private clipboard: Clipboard, () => this.deviceDetectorService.deviceInfo().deviceType
private dataService: DataService, );
private destroyRef: DestroyRef, private firstOrderDate: Date;
private deviceDetectorService: DeviceDetectorService,
private impersonationStorageService: ImpersonationStorageService, private readonly changeDetectorRef = inject(ChangeDetectorRef);
private snackBar: MatSnackBar, private readonly clipboard = inject(Clipboard);
private userService: UserService private readonly dataService = inject(DataService);
) { private readonly destroyRef = inject(DestroyRef);
private readonly deviceDetectorService = inject(DeviceDetectorService);
private readonly impersonationStorageService = inject(
ImpersonationStorageService
);
private readonly snackBar = inject(MatSnackBar);
private readonly userService = inject(UserService);
public constructor() {
const { benchmarks } = this.dataService.fetchInfo(); const { benchmarks } = this.dataService.fetchInfo();
this.benchmarks = benchmarks; this.benchmarks = benchmarks;
@ -123,14 +133,16 @@ export class GfAnalysisPageComponent implements OnInit {
? undefined ? undefined
: this.user?.settings?.savingsRate; : this.user?.settings?.savingsRate;
return this.mode === 'year' if (savingsRatePerMonth === undefined) {
return undefined;
}
return this.mode() === 'year'
? savingsRatePerMonth * 12 ? savingsRatePerMonth * 12
: savingsRatePerMonth; : savingsRatePerMonth;
} }
public ngOnInit() { public ngOnInit() {
this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType;
this.impersonationStorageService this.impersonationStorageService
.onChangeHasImpersonation() .onChangeHasImpersonation()
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
@ -158,7 +170,7 @@ export class GfAnalysisPageComponent implements OnInit {
}); });
} }
public onChangeBenchmark(symbolProfileId: string) { protected onChangeBenchmark(symbolProfileId: string) {
this.dataService this.dataService
.putUserSetting({ benchmark: symbolProfileId }) .putUserSetting({ benchmark: symbolProfileId })
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
@ -174,12 +186,12 @@ export class GfAnalysisPageComponent implements OnInit {
}); });
} }
public onChangeGroupBy(aMode: GroupBy) { protected onChangeGroupBy(aMode: GroupBy) {
this.mode = aMode; this.mode.set(aMode);
this.fetchDividendsAndInvestments(); this.fetchDividendsAndInvestments();
} }
public onCopyPromptToClipboard(mode: AiPromptMode) { protected onCopyPromptToClipboard(mode: AiPromptMode) {
if (mode === 'analysis') { if (mode === 'analysis') {
this.isLoadingAnalysisPrompt = true; this.isLoadingAnalysisPrompt = true;
} else if (mode === 'portfolio') { } else if (mode === 'portfolio') {
@ -210,7 +222,7 @@ export class GfAnalysisPageComponent implements OnInit {
window.open('https://duck.ai', '_blank'); window.open('https://duck.ai', '_blank');
}); });
this.actionsMenuButton.closeMenu(); this.actionsMenuButton().closeMenu();
if (mode === 'analysis') { if (mode === 'analysis') {
this.isLoadingAnalysisPrompt = false; this.isLoadingAnalysisPrompt = false;
@ -227,8 +239,8 @@ export class GfAnalysisPageComponent implements OnInit {
this.dataService this.dataService
.fetchDividends({ .fetchDividends({
filters: this.userService.getFilters(), filters: this.userService.getFilters(),
groupBy: this.mode, groupBy: this.mode(),
range: this.user?.settings?.dateRange range: this.user?.settings?.dateRange ?? DEFAULT_DATE_RANGE
}) })
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ dividends }) => { .subscribe(({ dividends }) => {
@ -242,15 +254,15 @@ export class GfAnalysisPageComponent implements OnInit {
this.dataService this.dataService
.fetchInvestments({ .fetchInvestments({
filters: this.userService.getFilters(), filters: this.userService.getFilters(),
groupBy: this.mode, groupBy: this.mode(),
range: this.user?.settings?.dateRange range: this.user?.settings?.dateRange ?? DEFAULT_DATE_RANGE
}) })
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ investments, streaks }) => { .subscribe(({ investments, streaks }) => {
this.investmentsByGroup = investments; this.investmentsByGroup = investments;
this.streaks = streaks; this.streaks = streaks;
this.unitCurrentStreak = this.unitCurrentStreak =
this.mode === 'year' this.mode() === 'year'
? this.streaks?.currentStreak === 1 ? this.streaks?.currentStreak === 1
? translate('YEAR') ? translate('YEAR')
: translate('YEARS') : translate('YEARS')
@ -258,7 +270,7 @@ export class GfAnalysisPageComponent implements OnInit {
? translate('MONTH') ? translate('MONTH')
: translate('MONTHS'); : translate('MONTHS');
this.unitLongestStreak = this.unitLongestStreak =
this.mode === 'year' this.mode() === 'year'
? this.streaks?.longestStreak === 1 ? this.streaks?.longestStreak === 1
? translate('YEAR') ? translate('YEAR')
: translate('YEARS') : translate('YEARS')
@ -278,7 +290,7 @@ export class GfAnalysisPageComponent implements OnInit {
this.dataService this.dataService
.fetchPortfolioPerformance({ .fetchPortfolioPerformance({
filters: this.userService.getFilters(), filters: this.userService.getFilters(),
range: this.user?.settings?.dateRange range: this.user?.settings?.dateRange ?? DEFAULT_DATE_RANGE
}) })
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ chart, firstOrderDate, performance }) => { .subscribe(({ chart, firstOrderDate, performance }) => {
@ -298,13 +310,16 @@ export class GfAnalysisPageComponent implements OnInit {
valueInPercentage, valueInPercentage,
valueWithCurrencyEffect valueWithCurrencyEffect
} }
] of chart.entries()) { ] of (chart ?? []).entries()) {
// Ignore first item where value is 0
if (index > 0 || this.user?.settings?.dateRange === 'max') { if (index > 0 || this.user?.settings?.dateRange === 'max') {
// Ignore first item where value is 0 if (totalInvestmentValueWithCurrencyEffect !== undefined) {
this.investments.push({ this.investments.push({
date, date,
investment: totalInvestmentValueWithCurrencyEffect investment: totalInvestmentValueWithCurrencyEffect
}); });
}
this.performanceDataItems.push({ this.performanceDataItems.push({
date, date,
value: isNumber(valueWithCurrencyEffect) value: isNumber(valueWithCurrencyEffect)
@ -320,7 +335,7 @@ export class GfAnalysisPageComponent implements OnInit {
} }
if ( if (
this.deviceType === 'mobile' && this.deviceType() === 'mobile' &&
this.performance.currentValueInBaseCurrency >= this.performance.currentValueInBaseCurrency >=
NUMERICAL_PRECISION_THRESHOLD_6_FIGURES NUMERICAL_PRECISION_THRESHOLD_6_FIGURES
) { ) {
@ -387,7 +402,7 @@ export class GfAnalysisPageComponent implements OnInit {
dataSource, dataSource,
symbol, symbol,
filters: this.userService.getFilters(), filters: this.userService.getFilters(),
range: this.user?.settings?.dateRange, range: this.user?.settings?.dateRange ?? DEFAULT_DATE_RANGE,
startDate: this.firstOrderDate startDate: this.firstOrderDate
}) })
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))

8
apps/client/src/app/pages/portfolio/analysis/analysis-page.html

@ -442,7 +442,7 @@
</div> </div>
<gf-toggle <gf-toggle
class="d-none d-lg-block" class="d-none d-lg-block"
[defaultValue]="mode" [defaultValue]="mode()"
[isLoading]="false" [isLoading]="false"
[options]="modeOptions" [options]="modeOptions"
(valueChange)="onChangeGroupBy($event.value)" (valueChange)="onChangeGroupBy($event.value)"
@ -476,7 +476,7 @@
[benchmarkDataItems]="investmentsByGroup" [benchmarkDataItems]="investmentsByGroup"
[benchmarkDataLabel]="investmentTimelineDataLabel" [benchmarkDataLabel]="investmentTimelineDataLabel"
[currency]="user?.settings?.baseCurrency" [currency]="user?.settings?.baseCurrency"
[groupBy]="mode" [groupBy]="mode()"
[isInPercentage]=" [isInPercentage]="
hasImpersonationId || user.settings.isRestrictedView hasImpersonationId || user.settings.isRestrictedView
" "
@ -501,7 +501,7 @@
</div> </div>
<gf-toggle <gf-toggle
class="d-none d-lg-block" class="d-none d-lg-block"
[defaultValue]="mode" [defaultValue]="mode()"
[isLoading]="false" [isLoading]="false"
[options]="modeOptions" [options]="modeOptions"
(valueChange)="onChangeGroupBy($event.value)" (valueChange)="onChangeGroupBy($event.value)"
@ -513,7 +513,7 @@
[benchmarkDataItems]="dividendsByGroup" [benchmarkDataItems]="dividendsByGroup"
[benchmarkDataLabel]="dividendTimelineDataLabel" [benchmarkDataLabel]="dividendTimelineDataLabel"
[currency]="user?.settings?.baseCurrency" [currency]="user?.settings?.baseCurrency"
[groupBy]="mode" [groupBy]="mode()"
[isInPercentage]=" [isInPercentage]="
hasImpersonationId || user.settings.isRestrictedView hasImpersonationId || user.settings.isRestrictedView
" "

Loading…
Cancel
Save