Browse Source

Task/improve type safety in holding detail dialog component (#7050)

Improve type safety
pull/7057/head
Kenrick Tandrian 6 days ago
committed by GitHub
parent
commit
5bb8bf834b
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 208
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
  2. 6
      libs/common/src/lib/interfaces/index.ts
  3. 6
      libs/common/src/lib/interfaces/line-chart-item.interface.ts
  4. 6
      libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts
  5. 25
      libs/ui/src/lib/services/data.service.ts

208
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts

@ -16,6 +16,7 @@ import {
EnhancedSymbolProfile, EnhancedSymbolProfile,
Filter, Filter,
LineChartItem, LineChartItem,
NullableLineChartItem,
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
@ -39,11 +40,16 @@ import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
DestroyRef, DestroyRef,
Inject, OnInit,
OnInit inject
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; import {
FormBuilder,
FormControl,
FormGroup,
ReactiveFormsModule
} from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatChipsModule } from '@angular/material/chips'; import { MatChipsModule } from '@angular/material/chips';
import { import {
@ -71,6 +77,7 @@ import {
swapVerticalOutline, swapVerticalOutline,
walletOutline walletOutline
} from 'ionicons/icons'; } from 'ionicons/icons';
import { isNumber } from 'lodash';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { switchMap } from 'rxjs/operators'; import { switchMap } from 'rxjs/operators';
@ -106,75 +113,78 @@ import { HoldingDetailDialogParams } from './interfaces/interfaces';
templateUrl: 'holding-detail-dialog.html' templateUrl: 'holding-detail-dialog.html'
}) })
export class GfHoldingDetailDialogComponent implements OnInit { export class GfHoldingDetailDialogComponent implements OnInit {
public activitiesCount: number; protected accounts: Account[];
public accounts: Account[]; protected activitiesCount: number;
public assetClass: string; protected assetClass: string;
public assetSubClass: string; protected assetSubClass: string;
public averagePrice: number; protected averagePrice: number;
public averagePricePrecision = 2; protected averagePricePrecision = 2;
public benchmarkDataItems: LineChartItem[]; protected benchmarkDataItems: NullableLineChartItem[];
public benchmarkLabel = $localize`Average Unit Price`; protected readonly benchmarkLabel = $localize`Average Unit Price`;
public countries: { protected countries: {
[code: string]: { name: string; value: number }; [code: string]: { name: string; value: number };
}; };
public dataProviderInfo: DataProviderInfo; protected dataProviderInfo: DataProviderInfo;
public dataSource: MatTableDataSource<Activity>; protected dataSource: MatTableDataSource<Activity>;
public dateOfFirstActivity: string; protected dateOfFirstActivity: Date;
public dividendInBaseCurrency: number; protected dividendInBaseCurrency: number;
public dividendInBaseCurrencyPrecision = 2; protected dividendInBaseCurrencyPrecision = 2;
public dividendYieldPercentWithCurrencyEffect: number; protected dividendYieldPercentWithCurrencyEffect: number;
public feeInBaseCurrency: number; protected feeInBaseCurrency: number;
public getCountryName = getCountryName; protected readonly getCountryName = getCountryName;
public hasPermissionToCreateOwnTag: boolean; protected hasPermissionToCreateOwnTag: boolean;
public hasPermissionToReadMarketDataOfOwnAssetProfile: boolean; protected hasPermissionToReadMarketDataOfOwnAssetProfile: boolean;
public historicalDataItems: LineChartItem[]; protected historicalDataItems: LineChartItem[];
public holdingForm: FormGroup; protected holdingForm: FormGroup<{
public investmentInBaseCurrencyWithCurrencyEffect: number; tags: FormControl<Tag[] | null>;
public investmentInBaseCurrencyWithCurrencyEffectPrecision = 2; }>;
public isUUID = isUUID; protected investmentInBaseCurrencyWithCurrencyEffect: number;
public marketDataItems: MarketData[] = []; protected investmentInBaseCurrencyWithCurrencyEffectPrecision = 2;
public marketPrice: number; protected readonly isUUID = isUUID;
public marketPriceMax: number; protected marketDataItems: MarketData[] = [];
public marketPriceMaxPrecision = 2; protected marketPrice: number;
public marketPriceMin: number; protected marketPriceMax: number;
public marketPriceMinPrecision = 2; protected marketPriceMaxPrecision = 2;
public marketPricePrecision = 2; protected marketPriceMin: number;
public netPerformance: number; protected marketPriceMinPrecision = 2;
public netPerformancePrecision = 2; protected marketPricePrecision = 2;
public netPerformancePercent: number; protected netPerformancePercentWithCurrencyEffect: number;
public netPerformancePercentWithCurrencyEffect: number; protected netPerformancePercentWithCurrencyEffectPrecision = 2;
public netPerformancePercentWithCurrencyEffectPrecision = 2; protected netPerformanceWithCurrencyEffect: number;
public netPerformanceWithCurrencyEffect: number; protected netPerformanceWithCurrencyEffectPrecision = 2;
public netPerformanceWithCurrencyEffectPrecision = 2; protected pageIndex = 0;
public pageIndex = 0; protected readonly pageSize = DEFAULT_PAGE_SIZE;
public pageSize = DEFAULT_PAGE_SIZE; protected quantity: number;
public quantity: number; protected quantityPrecision = 2;
public quantityPrecision = 2; protected reportDataGlitchMail: string;
public reportDataGlitchMail: string; protected readonly routerLinkAdminControlMarketData =
public routerLinkAdminControlMarketData =
internalRoutes.adminControl.subRoutes.marketData.routerLink; internalRoutes.adminControl.subRoutes.marketData.routerLink;
public sectors: { protected sectors: {
[name: string]: { name: string; value: number }; [name: string]: { name: string; value: number };
}; };
public sortColumn = 'date'; protected sortColumn = 'date';
public sortDirection: SortDirection = 'desc'; protected sortDirection: SortDirection = 'desc';
public SymbolProfile: EnhancedSymbolProfile; protected SymbolProfile: EnhancedSymbolProfile;
public tags: Tag[]; protected tagsAvailable: Tag[];
public tagsAvailable: Tag[]; protected readonly translate = translate;
public translate = translate; protected user: User;
public user: User; protected value: number;
public value: number;
protected readonly data = inject<HoldingDetailDialogParams>(MAT_DIALOG_DATA);
public constructor( protected readonly dialogRef = inject(
private changeDetectorRef: ChangeDetectorRef, MatDialogRef<GfHoldingDetailDialogComponent>
private dataService: DataService, );
private destroyRef: DestroyRef,
public dialogRef: MatDialogRef<GfHoldingDetailDialogComponent>, private tags: Tag[];
@Inject(MAT_DIALOG_DATA) public data: HoldingDetailDialogParams,
private formBuilder: FormBuilder, private readonly changeDetectorRef = inject(ChangeDetectorRef);
private router: Router, private readonly dataService = inject(DataService);
private userService: UserService private readonly destroyRef = inject(DestroyRef);
) { private readonly formBuilder = inject(FormBuilder);
private readonly router = inject(Router);
private readonly userService = inject(UserService);
public constructor() {
addIcons({ addIcons({
arrowDownCircleOutline, arrowDownCircleOutline,
createOutline, createOutline,
@ -190,12 +200,11 @@ export class GfHoldingDetailDialogComponent implements OnInit {
const filters = this.getActivityFilters(); const filters = this.getActivityFilters();
this.holdingForm = this.formBuilder.group({ this.holdingForm = this.formBuilder.group({
tags: [] as string[] tags: new FormControl<Tag[]>([])
}); });
this.holdingForm this.holdingForm.controls.tags.valueChanges
.get('tags') .pipe(takeUntilDestroyed(this.destroyRef))
.valueChanges.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((tags: Tag[]) => { .subscribe((tags: Tag[]) => {
const newTag = tags.find(({ id }) => { const newTag = tags.find(({ id }) => {
return id === undefined; return id === undefined;
@ -268,8 +277,6 @@ export class GfHoldingDetailDialogComponent implements OnInit {
marketPrice, marketPrice,
marketPriceMax, marketPriceMax,
marketPriceMin, marketPriceMin,
netPerformance,
netPerformancePercent,
netPerformancePercentWithCurrencyEffect, netPerformancePercentWithCurrencyEffect,
netPerformanceWithCurrencyEffect, netPerformanceWithCurrencyEffect,
quantity, quantity,
@ -290,7 +297,11 @@ export class GfHoldingDetailDialogComponent implements OnInit {
this.benchmarkDataItems = []; this.benchmarkDataItems = [];
this.countries = {}; this.countries = {};
this.dataProviderInfo = dataProviderInfo; this.dataProviderInfo = dataProviderInfo;
this.dateOfFirstActivity = dateOfFirstActivity;
if (dateOfFirstActivity) {
this.dateOfFirstActivity = dateOfFirstActivity;
}
this.dividendInBaseCurrency = dividendInBaseCurrency; this.dividendInBaseCurrency = dividendInBaseCurrency;
if ( if (
@ -318,12 +329,12 @@ export class GfHoldingDetailDialogComponent implements OnInit {
({ averagePrice, date, marketPrice }) => { ({ averagePrice, date, marketPrice }) => {
this.benchmarkDataItems.push({ this.benchmarkDataItems.push({
date, date,
value: averagePrice value: isNumber(averagePrice) ? averagePrice : null
}); });
return { return {
date, date,
value: marketPrice value: marketPrice ?? 0
}; };
} }
); );
@ -365,17 +376,6 @@ export class GfHoldingDetailDialogComponent implements OnInit {
this.marketPricePrecision = 0; this.marketPricePrecision = 0;
} }
this.netPerformance = netPerformance;
if (
this.data.deviceType === 'mobile' &&
this.netPerformance >= NUMERICAL_PRECISION_THRESHOLD_5_FIGURES
) {
this.netPerformancePrecision = 0;
}
this.netPerformancePercent = netPerformancePercent;
this.netPerformancePercentWithCurrencyEffect = this.netPerformancePercentWithCurrencyEffect =
netPerformancePercentWithCurrencyEffect; netPerformancePercentWithCurrencyEffect;
@ -456,16 +456,16 @@ export class GfHoldingDetailDialogComponent implements OnInit {
} }
} }
if (isToday(parseISO(this.dateOfFirstActivity))) { if (isToday(this.dateOfFirstActivity)) {
// Add average price // Add average price
this.historicalDataItems.push({ this.historicalDataItems.push({
date: this.dateOfFirstActivity, date: this.dateOfFirstActivity.toISOString(),
value: this.averagePrice value: this.averagePrice
}); });
// Add benchmark 1 // Add benchmark 1
this.benchmarkDataItems.push({ this.benchmarkDataItems.push({
date: this.dateOfFirstActivity, date: this.dateOfFirstActivity.toISOString(),
value: averagePrice value: averagePrice
}); });
@ -496,7 +496,7 @@ export class GfHoldingDetailDialogComponent implements OnInit {
if ( if (
this.benchmarkDataItems[0]?.value === undefined && this.benchmarkDataItems[0]?.value === undefined &&
isSameMonth(parseISO(this.dateOfFirstActivity), new Date()) isSameMonth(this.dateOfFirstActivity, new Date())
) { ) {
this.benchmarkDataItems[0].value = this.averagePrice; this.benchmarkDataItems[0].value = this.averagePrice;
} }
@ -526,7 +526,7 @@ export class GfHoldingDetailDialogComponent implements OnInit {
this.hasPermissionToCreateOwnTag = this.hasPermissionToCreateOwnTag =
hasPermission(this.user.permissions, permissions.createOwnTag) && hasPermission(this.user.permissions, permissions.createOwnTag) &&
this.user?.settings?.isExperimentalFeatures; (this.user?.settings?.isExperimentalFeatures ?? false);
this.tagsAvailable = this.tagsAvailable =
this.user?.tags?.map((tag) => { this.user?.tags?.map((tag) => {
@ -541,13 +541,13 @@ export class GfHoldingDetailDialogComponent implements OnInit {
}); });
} }
public onChangePage(page: PageEvent) { protected onChangePage(page: PageEvent) {
this.pageIndex = page.pageIndex; this.pageIndex = page.pageIndex;
this.fetchActivities(); this.fetchActivities();
} }
public onCloneActivity(aActivity: Activity) { protected onCloneActivity(aActivity: Activity) {
this.router.navigate( this.router.navigate(
internalRoutes.portfolio.subRoutes.activities.routerLink, internalRoutes.portfolio.subRoutes.activities.routerLink,
{ {
@ -558,22 +558,22 @@ export class GfHoldingDetailDialogComponent implements OnInit {
this.dialogRef.close(); this.dialogRef.close();
} }
public onClose() { protected onClose() {
this.dialogRef.close(); this.dialogRef.close();
} }
public onCloseHolding() { protected onCloseHolding() {
const today = new Date(); const today = new Date();
const activity: CreateOrderDto = { const activity: CreateOrderDto = {
accountId: this.accounts.length === 1 ? this.accounts[0].id : null, accountId: this.accounts.length === 1 ? this.accounts[0].id : undefined,
comment: null, comment: undefined,
currency: this.SymbolProfile.currency, currency: this.SymbolProfile?.currency ?? '',
dataSource: this.SymbolProfile.dataSource, dataSource: this.SymbolProfile?.dataSource,
date: today.toISOString(), date: today.toISOString(),
fee: 0, fee: 0,
quantity: this.quantity, quantity: this.quantity,
symbol: this.SymbolProfile.symbol, symbol: this.SymbolProfile?.symbol ?? '',
tags: this.tags.map(({ id }) => { tags: this.tags.map(({ id }) => {
return id; return id;
}), }),
@ -593,7 +593,7 @@ export class GfHoldingDetailDialogComponent implements OnInit {
}); });
} }
public onExport() { protected onExport() {
const activityIds = this.dataSource.data.map(({ id }) => { const activityIds = this.dataSource.data.map(({ id }) => {
return id; return id;
}); });
@ -613,13 +613,13 @@ export class GfHoldingDetailDialogComponent implements OnInit {
}); });
} }
public onMarketDataChanged(withRefresh = false) { protected onMarketDataChanged(withRefresh = false) {
if (withRefresh) { if (withRefresh) {
this.fetchMarketData(); this.fetchMarketData();
} }
} }
public onUpdateActivity(aActivity: Activity) { protected onUpdateActivity(aActivity: Activity) {
this.router.navigate( this.router.navigate(
internalRoutes.portfolio.subRoutes.activities.routerLink, internalRoutes.portfolio.subRoutes.activities.routerLink,
{ {

6
libs/common/src/lib/interfaces/index.ts

@ -25,7 +25,10 @@ import type { HoldingWithParents } from './holding-with-parents.interface';
import type { Holding } from './holding.interface'; import type { Holding } from './holding.interface';
import type { InfoItem } from './info-item.interface'; import type { InfoItem } from './info-item.interface';
import type { InvestmentItem } from './investment-item.interface'; import type { InvestmentItem } from './investment-item.interface';
import type { LineChartItem } from './line-chart-item.interface'; import type {
LineChartItem,
NullableLineChartItem
} from './line-chart-item.interface';
import type { LookupItem } from './lookup-item.interface'; import type { LookupItem } from './lookup-item.interface';
import type { MarketData } from './market-data.interface'; import type { MarketData } from './market-data.interface';
import type { PortfolioChart } from './portfolio-chart.interface'; import type { PortfolioChart } from './portfolio-chart.interface';
@ -158,6 +161,7 @@ export {
MarketData, MarketData,
MarketDataDetailsResponse, MarketDataDetailsResponse,
MarketDataOfMarketsResponse, MarketDataOfMarketsResponse,
NullableLineChartItem,
OAuthResponse, OAuthResponse,
PlatformsResponse, PlatformsResponse,
PortfolioChart, PortfolioChart,

6
libs/common/src/lib/interfaces/line-chart-item.interface.ts

@ -1,4 +1,6 @@
export interface LineChartItem { export interface LineChartItem<T = number> {
date: string; date: string;
value: number; value: T;
} }
export type NullableLineChartItem = LineChartItem<number | null>;

6
libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts

@ -68,7 +68,7 @@ export class GfHistoricalMarketDataEditorComponent
@Input() currency: string; @Input() currency: string;
@Input() dataSource: DataSource; @Input() dataSource: DataSource;
@Input() dateOfFirstActivity: string; @Input() dateOfFirstActivity: Date;
@Input() symbol: string; @Input() symbol: string;
@Input() user: User; @Input() user: User;
@ -124,7 +124,7 @@ export class GfHistoricalMarketDataEditorComponent
public ngOnChanges() { public ngOnChanges() {
if (this.dateOfFirstActivity) { if (this.dateOfFirstActivity) {
let date = parseISO(this.dateOfFirstActivity); let date = this.dateOfFirstActivity;
const missingMarketData: { date: Date; marketPrice?: number }[] = []; const missingMarketData: { date: Date; marketPrice?: number }[] = [];
@ -174,7 +174,7 @@ export class GfHistoricalMarketDataEditorComponent
const dates = Object.keys(this.marketDataByMonth).sort(); const dates = Object.keys(this.marketDataByMonth).sort();
const startDateString = first(dates); const startDateString = first(dates);
const startDate = min([ const startDate = min([
parseISO(this.dateOfFirstActivity), this.dateOfFirstActivity,
...(startDateString ? [parseISO(startDateString)] : []) ...(startDateString ? [parseISO(startDateString)] : [])
]); ]);
const endDateString = last(dates); const endDateString = last(dates);

25
libs/ui/src/lib/services/data.service.ts

@ -445,10 +445,27 @@ export class DataService {
}: { }: {
dataSource: DataSource; dataSource: DataSource;
symbol: string; symbol: string;
}) { }): Observable<
return this.http.get<PortfolioHoldingResponse>( Omit<PortfolioHoldingResponse, 'dateOfFirstActivity'> & {
`/api/v1/portfolio/holding/${dataSource}/${symbol}` dateOfFirstActivity: Date | undefined;
); }
> {
return this.http
.get<PortfolioHoldingResponse>(
`/api/v1/portfolio/holding/${dataSource}/${symbol}`
)
.pipe(
map((response) => {
const dateOfFirstActivity = response.dateOfFirstActivity
? parseISO(response.dateOfFirstActivity)
: undefined;
return {
...response,
dateOfFirstActivity
};
})
);
} }
public fetchInfo(): InfoItem { public fetchInfo(): InfoItem {

Loading…
Cancel
Save