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.
 
 
 
 
 

260 lines
8.2 KiB

import { UNKNOWN_KEY } from '@ghostfolio/common/config';
import { getCountryName, prettifySymbol } from '@ghostfolio/common/helper';
import {
InfoItem,
PortfolioPosition,
PublicPortfolioResponse
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { Market } from '@ghostfolio/common/types';
import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table/activities-table.component';
import { GfHoldingsTableComponent } from '@ghostfolio/ui/holdings-table/holdings-table.component';
import { translate } from '@ghostfolio/ui/i18n';
import { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.component';
import { DataService } from '@ghostfolio/ui/services';
import { GfValueComponent } from '@ghostfolio/ui/value';
import { GfWorldMapChartComponent } from '@ghostfolio/ui/world-map-chart';
import { HttpErrorResponse } from '@angular/common/http';
import {
ChangeDetectorRef,
Component,
computed,
CUSTOM_ELEMENTS_SCHEMA,
DestroyRef,
inject,
OnInit
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatTableDataSource } from '@angular/material/table';
import { ActivatedRoute, Router } from '@angular/router';
import { AssetClass } from '@prisma/client';
import { StatusCodes } from 'http-status-codes';
import { isNumber } from 'lodash';
import { DeviceDetectorService } from 'ngx-device-detector';
import { EMPTY } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Component({
host: { class: 'page' },
imports: [
GfActivitiesTableComponent,
GfHoldingsTableComponent,
GfPortfolioProportionChartComponent,
GfValueComponent,
GfWorldMapChartComponent,
MatButtonModule,
MatCardModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-public-page',
styleUrls: ['./public-page.scss'],
templateUrl: './public-page.html'
})
export class GfPublicPageComponent implements OnInit {
protected continents: {
[code: string]: { name: string; value: number };
};
protected countries: {
[code: string]: { name: string; value: number };
};
protected readonly defaultAlias = $localize`someone`;
protected readonly deviceType = computed(
() => this.deviceDetectorService.deviceInfo().deviceType
);
protected hasPermissionForSubscription: boolean;
protected holdings: PublicPortfolioResponse['holdings'][string][];
protected info: InfoItem;
protected latestActivitiesDataSource: MatTableDataSource<
PublicPortfolioResponse['latestActivities'][0]
>;
protected markets: {
[key in Market]: { id: Market; valueInPercentage: number };
};
protected readonly pageSize = Number.MAX_SAFE_INTEGER;
protected positions: {
[symbol: string]: Pick<
PortfolioPosition['assetProfile'],
'currency' | 'name'
> & {
value: number;
};
};
protected publicPortfolioDetails: PublicPortfolioResponse;
protected sectors: {
[name: string]: { name: string; value: number };
};
protected symbols: {
[name: string]: { name: string; symbol: string; value: number };
};
protected readonly UNKNOWN_KEY = UNKNOWN_KEY;
private readonly activatedRoute = inject(ActivatedRoute);
private readonly changeDetectorRef = inject(ChangeDetectorRef);
private readonly dataService = inject(DataService);
private readonly destroyRef = inject(DestroyRef);
private readonly deviceDetectorService = inject(DeviceDetectorService);
private readonly router = inject(Router);
private accessId: string;
public constructor() {
this.activatedRoute.params.subscribe((params) => {
this.accessId = params['id'];
});
this.info = this.dataService.fetchInfo();
this.hasPermissionForSubscription = hasPermission(
this.info?.globalPermissions,
permissions.enableSubscription
);
}
public ngOnInit() {
this.dataService
.fetchPublicPortfolio(this.accessId)
.pipe(
takeUntilDestroyed(this.destroyRef),
catchError((error: HttpErrorResponse) => {
if (error.status === StatusCodes.NOT_FOUND) {
console.error(error);
this.router.navigate(['/']);
}
return EMPTY;
})
)
.subscribe((portfolioPublicDetails) => {
this.publicPortfolioDetails = portfolioPublicDetails;
this.initializeAnalysisData();
this.latestActivitiesDataSource = new MatTableDataSource(
this.publicPortfolioDetails.latestActivities
);
this.changeDetectorRef.markForCheck();
});
}
private initializeAnalysisData() {
this.continents = {
[UNKNOWN_KEY]: {
name: UNKNOWN_KEY,
value: 0
}
};
this.countries = {
[UNKNOWN_KEY]: {
name: UNKNOWN_KEY,
value: 0
}
};
this.holdings = [];
this.markets = this.publicPortfolioDetails.markets;
this.positions = {};
this.sectors = {
[UNKNOWN_KEY]: {
name: UNKNOWN_KEY,
value: 0
}
};
this.symbols = {
[UNKNOWN_KEY]: {
name: UNKNOWN_KEY,
symbol: UNKNOWN_KEY,
value: 0
}
};
for (const [symbol, position] of Object.entries(
this.publicPortfolioDetails.holdings
)) {
this.holdings.push(position);
this.positions[symbol] = {
currency: position.assetProfile.currency,
name: position.assetProfile.name,
value: position.allocationInPercentage
};
if (position.assetProfile.assetClass !== AssetClass.LIQUIDITY) {
// Prepare analysis data by continents, countries, holdings and sectors except for liquidity
if (position.assetProfile.countries.length > 0) {
for (const country of position.assetProfile.countries) {
const { code, continent, weight } = country;
if (this.continents[continent]?.value) {
this.continents[continent].value +=
weight * (position.valueInBaseCurrency ?? 0);
} else {
this.continents[continent] = {
name: translate(continent),
value:
weight *
(this.publicPortfolioDetails.holdings[symbol]
.valueInBaseCurrency ?? 0)
};
}
if (this.countries[code]?.value) {
this.countries[code].value +=
weight * (position.valueInBaseCurrency ?? 0);
} else {
this.countries[code] = {
name: getCountryName({ code }),
value:
weight *
(this.publicPortfolioDetails.holdings[symbol]
.valueInBaseCurrency ?? 0)
};
}
}
} else {
this.continents[UNKNOWN_KEY].value +=
this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency ??
0;
this.countries[UNKNOWN_KEY].value +=
this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency ??
0;
}
if (position.assetProfile.sectors.length > 0) {
for (const sector of position.assetProfile.sectors) {
const { name, weight } = sector;
if (this.sectors[name]?.value) {
this.sectors[name].value +=
weight * (position.valueInBaseCurrency ?? 0);
} else {
this.sectors[name] = {
name: translate(name),
value:
weight *
(this.publicPortfolioDetails.holdings[symbol]
.valueInBaseCurrency ?? 0)
};
}
}
} else {
this.sectors[UNKNOWN_KEY].value +=
this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency ??
0;
}
}
this.symbols[prettifySymbol(symbol)] = {
name: position.assetProfile.name,
symbol: prettifySymbol(symbol),
value: isNumber(position.valueInBaseCurrency)
? position.valueInBaseCurrency
: (position.valueInPercentage ?? 0)
};
}
}
}