Browse Source

Feature/extend public portfolio page with latest activities (#5538)

* Extend public portfolio page with latest activities

* Update changelog

---------

Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
pull/5571/head
David Requeno 3 weeks ago
committed by GitHub
parent
commit
11c51698d7
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 48
      apps/api/src/app/endpoints/public/public.controller.ts
  3. 22
      apps/client/src/app/pages/public/public-page.component.ts
  4. 25
      apps/client/src/app/pages/public/public-page.html
  5. 18
      libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts

1
CHANGELOG.md

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added the symbol to the benchmark component
- Added the latest activities to the public page (experimental)
- Added pagination to the activities table of the activities import dialog
- Added an option to configure the account column of the activities table component

48
apps/api/src/app/endpoints/public/public.controller.ts

@ -1,6 +1,8 @@
import { AccessService } from '@ghostfolio/api/app/access/access.service';
import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
import { UserService } from '@ghostfolio/api/app/user/user.service';
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.interceptor';
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';
@ -18,6 +20,7 @@ import {
UseInterceptors
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Type as ActivityType } from '@prisma/client';
import { Big } from 'big.js';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
@ -27,12 +30,14 @@ export class PublicController {
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
@ -76,6 +81,48 @@ export class PublicController {
})
]);
const { activities } = await this.orderService.getOrders({
includeDrafts: false,
sortColumn: 'date',
sortDirection: 'desc',
take: 10,
types: [ActivityType.BUY, ActivityType.SELL],
userCurrency: user.settings?.settings.baseCurrency ?? DEFAULT_CURRENCY,
userId: user.id,
withExcludedAccountsAndActivities: false
});
// Experimental
const latestActivities = this.configurationService.get(
'ENABLE_FEATURE_SUBSCRIPTION'
)
? []
: activities.map(
({
currency,
date,
fee,
quantity,
SymbolProfile,
type,
unitPrice,
value,
valueInBaseCurrency
}) => {
return {
currency,
date,
fee,
quantity,
SymbolProfile,
type,
unitPrice,
value,
valueInBaseCurrency
};
}
);
Object.values(markets ?? {}).forEach((market) => {
delete market.valueInBaseCurrency;
});
@ -83,6 +130,7 @@ export class PublicController {
const publicPortfolioResponse: PublicPortfolioResponse = {
createdAt,
hasDetails,
latestActivities,
markets,
alias: access.alias,
holdings: {},

22
apps/client/src/app/pages/public/public-page.component.ts

@ -2,10 +2,13 @@ import { DataService } from '@ghostfolio/client/services/data.service';
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
import { 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 { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.component';
import { GfValueComponent } from '@ghostfolio/ui/value';
@ -20,6 +23,7 @@ import {
} from '@angular/core';
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';
@ -32,6 +36,7 @@ import { catchError, takeUntil } from 'rxjs/operators';
host: { class: 'page' },
imports: [
CommonModule,
GfActivitiesTableComponent,
GfHoldingsTableComponent,
GfPortfolioProportionChartComponent,
GfValueComponent,
@ -53,10 +58,16 @@ export class GfPublicPageComponent implements OnInit {
};
public defaultAlias = $localize`someone`;
public deviceType: string;
public hasPermissionForSubscription: boolean;
public holdings: PublicPortfolioResponse['holdings'][string][];
public info: InfoItem;
public latestActivitiesDataSource: MatTableDataSource<
PublicPortfolioResponse['latestActivities'][0]
>;
public markets: {
[key in Market]: { id: Market; valueInPercentage: number };
};
public pageSize = Number.MAX_SAFE_INTEGER;
public positions: {
[symbol: string]: Pick<PortfolioPosition, 'currency' | 'name'> & {
value: number;
@ -84,6 +95,13 @@ export class GfPublicPageComponent implements OnInit {
this.activatedRoute.params.subscribe((params) => {
this.accessId = params['id'];
});
this.info = this.dataService.fetchInfo();
this.hasPermissionForSubscription = hasPermission(
this.info?.globalPermissions,
permissions.enableSubscription
);
}
public ngOnInit() {
@ -107,6 +125,10 @@ export class GfPublicPageComponent implements OnInit {
this.initializeAnalysisData();
this.latestActivitiesDataSource = new MatTableDataSource(
this.publicPortfolioDetails.latestActivities
);
this.changeDetectorRef.markForCheck();
});
}

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

@ -203,6 +203,31 @@
</div>
</div>
}
<div class="row" [ngClass]="{ 'd-none': hasPermissionForSubscription }">
<div class="col-md-12">
<mat-card appearance="outlined">
<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]="latestActivitiesDataSource"
[deviceType]="deviceType"
[hasPermissionToCreateActivity]="false"
[hasPermissionToDeleteActivity]="false"
[hasPermissionToExportActivities]="false"
[hasPermissionToOpenDetails]="false"
[pageSize]="pageSize"
[showAccountColumn]="false"
[showActions]="false"
[sortDisabled]="true"
/>
</mat-card-content>
</mat-card>
</div>
</div>
<div class="row my-5">
<div class="col-md-10 offset-md-1">
<h2 class="h4 mb-1 text-center" i18n>

18
libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts

@ -1,5 +1,11 @@
import { PortfolioDetails, PortfolioPosition } from '..';
import { Market } from '../../types';
import {
EnhancedSymbolProfile,
PortfolioDetails,
PortfolioPosition
} from '@ghostfolio/common/interfaces';
import { Market } from '@ghostfolio/common/types';
import { Order } from '@prisma/client';
export interface PublicPortfolioResponse extends PublicPortfolioResponseV1 {
alias?: string;
@ -23,6 +29,14 @@ export interface PublicPortfolioResponse extends PublicPortfolioResponseV1 {
| 'valueInPercentage'
>;
};
latestActivities: (Pick<
Order,
'currency' | 'date' | 'fee' | 'quantity' | 'type' | 'unitPrice'
> & {
SymbolProfile?: EnhancedSymbolProfile;
value: number;
valueInBaseCurrency: number;
})[];
markets: {
[key in Market]: Pick<
PortfolioDetails['markets'][key],

Loading…
Cancel
Save