diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f65ade6a..607701318 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/apps/api/src/app/endpoints/public/public.controller.ts b/apps/api/src/app/endpoints/public/public.controller.ts index 0175b6ce8..0f3ba4682 100644 --- a/apps/api/src/app/endpoints/public/public.controller.ts +++ b/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: {}, 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..55e2a122a 100644 --- a/apps/client/src/app/pages/public/public-page.component.ts +++ b/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 & { 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(); }); } diff --git a/apps/client/src/app/pages/public/public-page.html b/apps/client/src/app/pages/public/public-page.html index 004149ca3..079566e0d 100644 --- a/apps/client/src/app/pages/public/public-page.html +++ b/apps/client/src/app/pages/public/public-page.html @@ -203,6 +203,31 @@ } +
+
+ + + 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..cb06800be 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,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],