diff --git a/apps/api/src/app/endpoints/public/public.controller.ts b/apps/api/src/app/endpoints/public/public.controller.ts index 0175b6ce8..1478dd245 100644 --- a/apps/api/src/app/endpoints/public/public.controller.ts +++ b/apps/api/src/app/endpoints/public/public.controller.ts @@ -1,9 +1,3 @@ -import { AccessService } from '@ghostfolio/api/app/access/access.service'; -import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; -import { UserService } from '@ghostfolio/api/app/user/user.service'; -import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor'; -import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; -import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { DEFAULT_CURRENCY } from '@ghostfolio/common/config'; import { getSum } from '@ghostfolio/common/helper'; import { PublicPortfolioResponse } from '@ghostfolio/common/interfaces'; @@ -21,18 +15,29 @@ import { REQUEST } from '@nestjs/core'; import { Big } from 'big.js'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; +import { RedactValuesInResponseInterceptor } from '../../../interceptors/redact-values-in-response/redact-values-in-response.interceptor'; +import { TransformDataSourceInResponseInterceptor } from '../../../interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor'; +import { ConfigurationService } from '../../../services/configuration/configuration.service'; +import { ExchangeRateDataService } from '../../../services/exchange-rate-data/exchange-rate-data.service'; +import { AccessService } from '../../access/access.service'; +import { OrderService } from '../../order/order.service'; +import { PortfolioService } from '../../portfolio/portfolio.service'; +import { UserService } from '../../user/user.service'; + @Controller('public') export class PublicController { public constructor( private readonly accessService: AccessService, private readonly configurationService: ConfigurationService, private readonly exchangeRateDataService: ExchangeRateDataService, + private readonly _orderService: OrderService, private readonly portfolioService: PortfolioService, @Inject(REQUEST) private readonly request: RequestWithUser, private readonly userService: UserService ) {} @Get(':accessId/portfolio') + @UseInterceptors(RedactValuesInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor) public async getPublicPortfolio( @Param('accessId') accessId @@ -56,26 +61,69 @@ export class PublicController { hasDetails = user.subscription.type === 'Premium'; } + const detailsPromise = this.portfolioService.getDetails({ + impersonationId: access.userId, + userId: user.id, + withMarkets: true + }); + + const performance1dPromise = this.portfolioService.getPerformance({ + dateRange: '1d', + impersonationId: undefined, + userId: user.id + }); + + const performanceMaxPromise = this.portfolioService.getPerformance({ + dateRange: 'max', + impersonationId: undefined, + userId: user.id + }); + + const performanceYtdPromise = this.portfolioService.getPerformance({ + dateRange: 'ytd', + impersonationId: undefined, + userId: user.id + }); + + const latestActivitiesPromise = this._orderService.getOrders({ + includeDrafts: false, + take: 10, + sortColumn: 'date', + sortDirection: 'desc', + userCurrency: + this.request.user?.settings?.settings.baseCurrency ?? DEFAULT_CURRENCY, + userId: user.id, + withExcludedAccountsAndActivities: false + }); + const [ { createdAt, holdings, markets }, { performance: performance1d }, { performance: performanceMax }, { performance: performanceYtd } ] = await Promise.all([ - this.portfolioService.getDetails({ - impersonationId: access.userId, - userId: user.id, - withMarkets: true - }), - ...['1d', 'max', 'ytd'].map((dateRange) => { - return this.portfolioService.getPerformance({ - dateRange, - impersonationId: undefined, - userId: user.id - }); - }) + detailsPromise, + performance1dPromise, + performanceMaxPromise, + performanceYtdPromise ]); + const { activities } = await latestActivitiesPromise; + const latestActivities = activities.map((a) => { + return { + account: a.account + ? { name: a.account.name, currency: a.account.currency } + : undefined, + dataSource: a.SymbolProfile?.dataSource, + date: a.date, + name: a.SymbolProfile?.name ?? '', + quantity: a.quantity, + symbol: a.SymbolProfile?.symbol ?? '', + type: a.type, + unitPrice: a.unitPrice + }; + }); + Object.values(markets ?? {}).forEach((market) => { delete market.valueInBaseCurrency; }); @@ -83,6 +131,7 @@ export class PublicController { const publicPortfolioResponse: PublicPortfolioResponse = { createdAt, hasDetails, + latestActivities, markets, alias: access.alias, holdings: {}, diff --git a/apps/client/src/app/pages/public/public-page.component.ts b/apps/client/src/app/pages/public/public-page.component.ts index ea11dd25f..4a1a18fdb 100644 --- a/apps/client/src/app/pages/public/public-page.component.ts +++ b/apps/client/src/app/pages/public/public-page.component.ts @@ -1,4 +1,3 @@ -import { DataService } from '@ghostfolio/client/services/data.service'; import { UNKNOWN_KEY } from '@ghostfolio/common/config'; import { prettifySymbol } from '@ghostfolio/common/helper'; import { @@ -6,6 +5,7 @@ import { PublicPortfolioResponse } from '@ghostfolio/common/interfaces'; 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 { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.component'; import { GfValueComponent } from '@ghostfolio/ui/value'; @@ -28,10 +28,13 @@ import { DeviceDetectorService } from 'ngx-device-detector'; import { EMPTY, Subject } from 'rxjs'; import { catchError, takeUntil } from 'rxjs/operators'; +import { DataService } from '../../services/data.service'; + @Component({ host: { class: 'page' }, imports: [ CommonModule, + GfActivitiesTableComponent, GfHoldingsTableComponent, GfPortfolioProportionChartComponent, GfValueComponent, diff --git a/apps/client/src/app/pages/public/public-page.html b/apps/client/src/app/pages/public/public-page.html index 004149ca3..28f7f7a94 100644 --- a/apps/client/src/app/pages/public/public-page.html +++ b/apps/client/src/app/pages/public/public-page.html @@ -220,3 +220,30 @@ +@if (publicPortfolioDetails?.latestActivities?.length > 0) { +
+
+ + + Latest activities + + + + + +
+
+} diff --git a/libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts b/libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts index 7a98e3c8d..5800b965b 100644 --- a/libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts +++ b/libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts @@ -1,5 +1,7 @@ +import { DataSource } from '@prisma/client'; + import { PortfolioDetails, PortfolioPosition } from '..'; -import { Market } from '../../types'; +import { AccountWithPlatform, Market } from '../../types'; export interface PublicPortfolioResponse extends PublicPortfolioResponseV1 { alias?: string; @@ -23,6 +25,16 @@ export interface PublicPortfolioResponse extends PublicPortfolioResponseV1 { | 'valueInPercentage' >; }; + latestActivities?: { + account?: Pick; + date: Date; + name: string; + quantity: number; + symbol: string; + type: string; + unitPrice: number; + dataSource: DataSource; + }[]; markets: { [key in Market]: Pick< PortfolioDetails['markets'][key],