Browse Source

Feature/extend public portfolio page with latest activities

- Add latestActivities to PublicPortfolioResponse interface

- Inject OrderService in PublicController

- Query last 10 activities with proper filters (exclude drafts and EXCLUDE_FROM_ANALYSIS)

- Apply RedactValuesInResponseInterceptor for data protection

- Map Activity[] to public DTO format

- Add Latest activities section to public page template

- Reuse gf-activities-table component with public mode settings
pull/5538/head
David Requeno 5 days ago
parent
commit
33bbf8ccd7
  1. 85
      apps/api/src/app/endpoints/public/public.controller.ts
  2. 5
      apps/client/src/app/pages/public/public-page.component.ts
  3. 27
      apps/client/src/app/pages/public/public-page.html
  4. 14
      libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts

85
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: {},

5
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,

27
apps/client/src/app/pages/public/public-page.html

@ -220,3 +220,30 @@
</div>
</div>
</div>
@if (publicPortfolioDetails?.latestActivities?.length > 0) {
<div class="row">
<div class="col-md-12">
<mat-card appearance="outlined" class="mb-3">
<mat-card-header class="overflow-hidden w-100">
<mat-card-title class="text-truncate" i18n
>Latest activities</mat-card-title
>
</mat-card-header>
<mat-card-content>
<gf-activities-table
[dataSource]="{ data: publicPortfolioDetails.latestActivities } as any"
[deviceType]="deviceType"
[hasPermissionToCreateActivity]="false"
[hasPermissionToDeleteActivity]="false"
[hasPermissionToExportActivities]="false"
[hasPermissionToOpenDetails]="false"
[pageSize]="10"
[showActions]="false"
[showCheckbox]="false"
[showNameColumn]="true"
/>
</mat-card-content>
</mat-card>
</div>
</div>
}

14
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<AccountWithPlatform, 'name' | 'currency'>;
date: Date;
name: string;
quantity: number;
symbol: string;
type: string;
unitPrice: number;
dataSource: DataSource;
}[];
markets: {
[key in Market]: Pick<
PortfolioDetails['markets'][key],

Loading…
Cancel
Save