diff --git a/CHANGELOG.md b/CHANGELOG.md index 20277377..6fd6c712 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added + +- Added support to exclude an account from analysis + ## 1.197.0 - 24.09.2022 ### Added diff --git a/apps/api/src/app/account/account.controller.ts b/apps/api/src/app/account/account.controller.ts index 524e36f5..3ef671cc 100644 --- a/apps/api/src/app/account/account.controller.ts +++ b/apps/api/src/app/account/account.controller.ts @@ -96,7 +96,9 @@ export class AccountController { let accountsWithAggregations = await this.portfolioService.getAccountsWithAggregations( - impersonationUserId || this.request.user.id + impersonationUserId || this.request.user.id, + undefined, + true ); if ( @@ -139,7 +141,8 @@ export class AccountController { let accountsWithAggregations = await this.portfolioService.getAccountsWithAggregations( impersonationUserId || this.request.user.id, - [{ id, type: 'ACCOUNT' }] + [{ id, type: 'ACCOUNT' }], + true ); if ( diff --git a/apps/api/src/app/account/account.service.ts b/apps/api/src/app/account/account.service.ts index b9b65716..7c10fc31 100644 --- a/apps/api/src/app/account/account.service.ts +++ b/apps/api/src/app/account/account.service.ts @@ -107,15 +107,23 @@ export class AccountService { public async getCashDetails({ currency, filters = [], - userId + userId, + withExcludedAccounts = false }: { currency: string; filters?: Filter[]; userId: string; + withExcludedAccounts?: boolean; }): Promise { let totalCashBalanceInBaseCurrency = new Big(0); - const where: Prisma.AccountWhereInput = { userId }; + const where: Prisma.AccountWhereInput = { + userId + }; + + if (withExcludedAccounts === false) { + where.isExcluded = false; + } const { ACCOUNT: filtersByAccount, diff --git a/apps/api/src/app/account/create-account.dto.ts b/apps/api/src/app/account/create-account.dto.ts index f53a20e7..3ea13e20 100644 --- a/apps/api/src/app/account/create-account.dto.ts +++ b/apps/api/src/app/account/create-account.dto.ts @@ -1,5 +1,11 @@ import { AccountType } from '@prisma/client'; -import { IsNumber, IsString, ValidateIf } from 'class-validator'; +import { + IsBoolean, + IsNumber, + IsOptional, + IsString, + ValidateIf +} from 'class-validator'; export class CreateAccountDto { @IsString() @@ -11,6 +17,10 @@ export class CreateAccountDto { @IsString() currency: string; + @IsBoolean() + @IsOptional() + isExcluded?: boolean; + @IsString() name: string; diff --git a/apps/api/src/app/account/update-account.dto.ts b/apps/api/src/app/account/update-account.dto.ts index 343f46a7..0b573760 100644 --- a/apps/api/src/app/account/update-account.dto.ts +++ b/apps/api/src/app/account/update-account.dto.ts @@ -1,5 +1,11 @@ import { AccountType } from '@prisma/client'; -import { IsNumber, IsString, ValidateIf } from 'class-validator'; +import { + IsBoolean, + IsNumber, + IsOptional, + IsString, + ValidateIf +} from 'class-validator'; export class UpdateAccountDto { @IsString() @@ -14,6 +20,10 @@ export class UpdateAccountDto { @IsString() id: string; + @IsBoolean() + @IsOptional() + isExcluded?: boolean; + @IsString() name: string; diff --git a/apps/api/src/app/order/order.controller.ts b/apps/api/src/app/order/order.controller.ts index e3cafefc..a0c606b8 100644 --- a/apps/api/src/app/order/order.controller.ts +++ b/apps/api/src/app/order/order.controller.ts @@ -109,7 +109,8 @@ export class OrderController { filters, userCurrency, includeDrafts: true, - userId: impersonationUserId || this.request.user.id + userId: impersonationUserId || this.request.user.id, + withExcludedAccounts: true }); if ( diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index bf549200..b95c9697 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -189,13 +189,15 @@ export class OrderService { includeDrafts = false, types, userCurrency, - userId + userId, + withExcludedAccounts = false }: { filters?: Filter[]; includeDrafts?: boolean; types?: TypeOfOrder[]; userCurrency: string; userId: string; + withExcludedAccounts?: boolean; }): Promise { const where: Prisma.OrderWhereInput = { userId }; @@ -284,24 +286,28 @@ export class OrderService { }, orderBy: { date: 'asc' } }) - ).map((order) => { - const value = new Big(order.quantity).mul(order.unitPrice).toNumber(); + ) + .filter((order) => { + return withExcludedAccounts || order.Account?.isExcluded === false; + }) + .map((order) => { + const value = new Big(order.quantity).mul(order.unitPrice).toNumber(); - return { - ...order, - value, - feeInBaseCurrency: this.exchangeRateDataService.toCurrency( - order.fee, - order.SymbolProfile.currency, - userCurrency - ), - valueInBaseCurrency: this.exchangeRateDataService.toCurrency( + return { + ...order, value, - order.SymbolProfile.currency, - userCurrency - ) - }; - }); + feeInBaseCurrency: this.exchangeRateDataService.toCurrency( + order.fee, + order.SymbolProfile.currency, + userCurrency + ), + valueInBaseCurrency: this.exchangeRateDataService.toCurrency( + value, + order.SymbolProfile.currency, + userCurrency + ) + }; + }); } public async updateOrder({ diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 66cd408c..4a017388 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -148,12 +148,15 @@ export class PortfolioController { }) ]; + let portfolioSummary: PortfolioSummary; + const { accounts, filteredValueInBaseCurrency, filteredValueInPercentage, hasErrors, holdings, + summary, totalValueInBaseCurrency } = await this.portfolioService.getDetails( impersonationId, @@ -166,6 +169,8 @@ export class PortfolioController { hasError = true; } + portfolioSummary = summary; + if ( impersonationId || this.userService.isRestrictedView(this.request.user) @@ -199,6 +204,22 @@ export class PortfolioController { accounts[name].current = current / totalValue; accounts[name].original = original / totalInvestment; } + + portfolioSummary = nullifyValuesInObject(summary, [ + 'cash', + 'committedFunds', + 'currentGrossPerformance', + 'currentNetPerformance', + 'currentValue', + 'dividend', + 'emergencyFund', + 'excludedAccountsAndActivities', + 'fees', + 'items', + 'netWorth', + 'totalBuy', + 'totalSell' + ]); } let hasDetails = true; @@ -224,7 +245,8 @@ export class PortfolioController { filteredValueInPercentage, hasError, holdings, - totalValueInBaseCurrency + totalValueInBaseCurrency, + summary: hasDetails ? portfolioSummary : undefined }; } @@ -420,46 +442,6 @@ export class PortfolioController { return portfolioPublicDetails; } - @Get('summary') - @UseGuards(AuthGuard('jwt')) - public async getSummary( - @Headers('impersonation-id') impersonationId - ): Promise { - if ( - this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && - this.request.user.subscription.type === 'Basic' - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - - let summary = await this.portfolioService.getSummary(impersonationId); - - if ( - impersonationId || - this.userService.isRestrictedView(this.request.user) - ) { - summary = nullifyValuesInObject(summary, [ - 'cash', - 'committedFunds', - 'currentGrossPerformance', - 'currentNetPerformance', - 'currentValue', - 'dividend', - 'emergencyFund', - 'fees', - 'items', - 'netWorth', - 'totalBuy', - 'totalSell' - ]); - } - - return summary; - } - @Get('position/:dataSource/:symbol') @UseInterceptors(TransformDataSourceInRequestInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor) diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index c38e2a8d..a49bda91 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -50,8 +50,11 @@ import type { import { Inject, Injectable } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { + Account, AssetClass, DataSource, + Order, + Platform, Prisma, Tag, Type as TypeOfOrder @@ -106,7 +109,8 @@ export class PortfolioService { public async getAccounts( aUserId: string, - aFilters?: Filter[] + aFilters?: Filter[], + withExcludedAccounts = false ): Promise { const where: Prisma.AccountWhereInput = { userId: aUserId }; @@ -120,7 +124,13 @@ export class PortfolioService { include: { Order: true, Platform: true }, orderBy: { name: 'asc' } }), - this.getDetails(aUserId, aUserId, undefined, aFilters) + this.getDetails( + aUserId, + aUserId, + undefined, + aFilters, + withExcludedAccounts + ) ]); const userCurrency = this.request.user.Settings.settings.baseCurrency; @@ -160,9 +170,14 @@ export class PortfolioService { public async getAccountsWithAggregations( aUserId: string, - aFilters?: Filter[] + aFilters?: Filter[], + withExcludedAccounts = false ): Promise { - const accounts = await this.getAccounts(aUserId, aFilters); + const accounts = await this.getAccounts( + aUserId, + aFilters, + withExcludedAccounts + ); let totalBalanceInBaseCurrency = new Big(0); let totalValueInBaseCurrency = new Big(0); let transactionCount = 0; @@ -410,7 +425,8 @@ export class PortfolioService { aImpersonationId: string, aUserId: string, aDateRange: DateRange = 'max', - aFilters?: Filter[] + aFilters?: Filter[], + withExcludedAccounts = false ): Promise { const userId = await this.getUserId(aImpersonationId, aUserId); const user = await this.userService.user({ id: userId }); @@ -426,6 +442,7 @@ export class PortfolioService { const { orders, portfolioOrders, transactionPoints } = await this.getTransactionPoints({ userId, + withExcludedAccounts, filters: aFilters }); @@ -580,6 +597,7 @@ export class PortfolioService { portfolioItemsNow, userCurrency, userId, + withExcludedAccounts, filters: aFilters }); @@ -588,6 +606,7 @@ export class PortfolioService { return { accounts, holdings, + summary, filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(), filteredValueInPercentage: summary.netWorth ? filteredValueInBaseCurrency.div(summary.netWorth).toNumber() @@ -606,7 +625,11 @@ export class PortfolioService { const userId = await this.getUserId(aImpersonationId, this.request.user.id); const orders = ( - await this.orderService.getOrders({ userCurrency, userId }) + await this.orderService.getOrders({ + userCurrency, + userId, + withExcludedAccounts: true + }) ).filter(({ SymbolProfile }) => { return ( SymbolProfile.dataSource === aDataSource && @@ -1181,74 +1204,6 @@ export class PortfolioService { }; } - public async getSummary(aImpersonationId: string): Promise { - const userCurrency = this.request.user.Settings.settings.baseCurrency; - const userId = await this.getUserId(aImpersonationId, this.request.user.id); - const user = await this.userService.user({ id: userId }); - - const performanceInformation = await this.getPerformance(aImpersonationId); - - const { balanceInBaseCurrency } = await this.accountService.getCashDetails({ - userId, - currency: userCurrency - }); - const orders = await this.orderService.getOrders({ - userCurrency, - userId - }); - const dividend = this.getDividend(orders).toNumber(); - const emergencyFund = new Big( - (user.Settings?.settings as UserSettings)?.emergencyFund ?? 0 - ); - const fees = this.getFees(orders).toNumber(); - const firstOrderDate = orders[0]?.date; - const items = this.getItems(orders).toNumber(); - - const totalBuy = this.getTotalByType(orders, userCurrency, 'BUY'); - const totalSell = this.getTotalByType(orders, userCurrency, 'SELL'); - - const cash = new Big(balanceInBaseCurrency).minus(emergencyFund).toNumber(); - const committedFunds = new Big(totalBuy).minus(totalSell); - - const netWorth = new Big(balanceInBaseCurrency) - .plus(performanceInformation.performance.currentValue) - .plus(items) - .toNumber(); - - const daysInMarket = differenceInDays(new Date(), firstOrderDate); - - const annualizedPerformancePercent = new PortfolioCalculator({ - currency: userCurrency, - currentRateService: this.currentRateService, - orders: [] - }) - .getAnnualizedPerformancePercent({ - daysInMarket, - netPerformancePercent: new Big( - performanceInformation.performance.currentNetPerformancePercent - ) - }) - ?.toNumber(); - - return { - ...performanceInformation.performance, - annualizedPerformancePercent, - cash, - dividend, - fees, - firstOrderDate, - items, - netWorth, - totalBuy, - totalSell, - committedFunds: committedFunds.toNumber(), - emergencyFund: emergencyFund.toNumber(), - ordersCount: orders.filter((order) => { - return order.type === 'BUY' || order.type === 'SELL'; - }).length - }; - } - private async getCashPositions({ cashDetails, emergencyFund, @@ -1424,14 +1379,117 @@ export class PortfolioService { return portfolioStart; } + private async getSummary( + aImpersonationId: string + ): Promise { + const userCurrency = this.request.user.Settings.settings.baseCurrency; + const userId = await this.getUserId(aImpersonationId, this.request.user.id); + const user = await this.userService.user({ id: userId }); + + const performanceInformation = await this.getPerformance(aImpersonationId); + + const { balanceInBaseCurrency } = await this.accountService.getCashDetails({ + userId, + currency: userCurrency + }); + const orders = await this.orderService.getOrders({ + userCurrency, + userId + }); + + const excludedActivities = ( + await this.orderService.getOrders({ + userCurrency, + userId, + withExcludedAccounts: true + }) + ).filter(({ Account: account }) => { + return account?.isExcluded ?? false; + }); + + const dividend = this.getDividend(orders).toNumber(); + const emergencyFund = new Big( + (user.Settings?.settings as UserSettings)?.emergencyFund ?? 0 + ); + const fees = this.getFees(orders).toNumber(); + const firstOrderDate = orders[0]?.date; + const items = this.getItems(orders).toNumber(); + + const totalBuy = this.getTotalByType(orders, userCurrency, 'BUY'); + const totalSell = this.getTotalByType(orders, userCurrency, 'SELL'); + + const cash = new Big(balanceInBaseCurrency).minus(emergencyFund).toNumber(); + const committedFunds = new Big(totalBuy).minus(totalSell); + const totalOfExcludedActivities = new Big( + this.getTotalByType(excludedActivities, userCurrency, 'BUY') + ).minus(this.getTotalByType(excludedActivities, userCurrency, 'SELL')); + + const cashDetailsWithExcludedAccounts = + await this.accountService.getCashDetails({ + userId, + currency: userCurrency, + withExcludedAccounts: true + }); + + const excludedBalanceInBaseCurrency = new Big( + cashDetailsWithExcludedAccounts.balanceInBaseCurrency + ).minus(balanceInBaseCurrency); + + const excludedAccountsAndActivities = excludedBalanceInBaseCurrency + .plus(totalOfExcludedActivities) + .toNumber(); + + const netWorth = new Big(balanceInBaseCurrency) + .plus(performanceInformation.performance.currentValue) + .plus(items) + .plus(excludedAccountsAndActivities) + .toNumber(); + + const daysInMarket = differenceInDays(new Date(), firstOrderDate); + + const annualizedPerformancePercent = new PortfolioCalculator({ + currency: userCurrency, + currentRateService: this.currentRateService, + orders: [] + }) + .getAnnualizedPerformancePercent({ + daysInMarket, + netPerformancePercent: new Big( + performanceInformation.performance.currentNetPerformancePercent + ) + }) + ?.toNumber(); + + return { + ...performanceInformation.performance, + annualizedPerformancePercent, + cash, + dividend, + excludedAccountsAndActivities, + fees, + firstOrderDate, + items, + netWorth, + totalBuy, + totalSell, + committedFunds: committedFunds.toNumber(), + emergencyFund: emergencyFund.toNumber(), + ordersCount: orders.filter((order) => { + return order.type === 'BUY' || order.type === 'SELL'; + }).length + }; + } + private async getTransactionPoints({ filters, includeDrafts = false, - userId + userId, + withExcludedAccounts }: { filters?: Filter[]; includeDrafts?: boolean; userId: string; + withExcludedAccounts?: boolean; }): Promise<{ transactionPoints: TransactionPoint[]; orders: OrderWithAccount[]; @@ -1445,6 +1503,7 @@ export class PortfolioService { includeDrafts, userCurrency, userId, + withExcludedAccounts, types: ['BUY', 'SELL'] }); @@ -1496,17 +1555,22 @@ export class PortfolioService { orders, portfolioItemsNow, userCurrency, - userId + userId, + withExcludedAccounts }: { filters?: Filter[]; orders: OrderWithAccount[]; portfolioItemsNow: { [p: string]: TimelinePosition }; userCurrency: string; userId: string; + withExcludedAccounts?: boolean; }) { const accounts: PortfolioDetails['accounts'] = {}; - let currentAccounts = []; + let currentAccounts: (Account & { + Order?: Order[]; + Platform?: Platform; + })[] = []; if (filters.length === 0) { currentAccounts = await this.accountService.getAccounts(userId); @@ -1526,6 +1590,10 @@ export class PortfolioService { }); } + currentAccounts = currentAccounts.filter((account) => { + return withExcludedAccounts || account.isExcluded === false; + }); + for (const account of currentAccounts) { const ordersByAccount = orders.filter(({ accountId }) => { return accountId === account.id; diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts index ca2f229e..a0c11bce 100644 --- a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts +++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts @@ -61,7 +61,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit { .subscribe(({ accountType, name, Platform, valueInBaseCurrency }) => { this.accountType = accountType; this.name = name; - this.platformName = Platform?.name; + this.platformName = Platform?.name ?? '-'; this.valueInBaseCurrency = valueInBaseCurrency; this.changeDetectorRef.markForCheck(); diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html index edf111ff..53a9c76e 100644 --- a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html +++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html @@ -21,10 +21,12 @@
- Account Type + Account Type
- Platform + Platform
diff --git a/apps/client/src/app/components/home-summary/home-summary.component.ts b/apps/client/src/app/components/home-summary/home-summary.component.ts index 106aba6c..f10f09fe 100644 --- a/apps/client/src/app/components/home-summary/home-summary.component.ts +++ b/apps/client/src/app/components/home-summary/home-summary.component.ts @@ -1,8 +1,18 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { + MatSnackBar, + MatSnackBarRef, + TextOnlySnackBar +} from '@angular/material/snack-bar'; +import { Router } from '@angular/router'; import { DataService } from '@ghostfolio/client/services/data.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; -import { PortfolioSummary, User } from '@ghostfolio/common/interfaces'; +import { + InfoItem, + PortfolioSummary, + User +} from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -14,8 +24,11 @@ import { takeUntil } from 'rxjs/operators'; }) export class HomeSummaryComponent implements OnDestroy, OnInit { public hasImpersonationId: boolean; + public hasPermissionForSubscription: boolean; public hasPermissionToUpdateUserSettings: boolean; + public info: InfoItem; public isLoading = true; + public snackBarRef: MatSnackBarRef; public summary: PortfolioSummary; public user: User; @@ -25,8 +38,17 @@ export class HomeSummaryComponent implements OnDestroy, OnInit { private changeDetectorRef: ChangeDetectorRef, private dataService: DataService, private impersonationStorageService: ImpersonationStorageService, + private router: Router, + private snackBar: MatSnackBar, private userService: UserService ) { + this.info = this.dataService.fetchInfo(); + + this.hasPermissionForSubscription = hasPermission( + this.info?.globalPermissions, + permissions.enableSubscription + ); + this.userService.stateChanged .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((state) => { @@ -50,8 +72,6 @@ export class HomeSummaryComponent implements OnDestroy, OnInit { .subscribe((aId) => { this.hasImpersonationId = !!aId; }); - - this.update(); } public onChangeEmergencyFund(emergencyFund: number) { @@ -81,12 +101,30 @@ export class HomeSummaryComponent implements OnDestroy, OnInit { this.isLoading = true; this.dataService - .fetchPortfolioSummary() + .fetchPortfolioDetails({}) .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((response) => { - this.summary = response; + .subscribe(({ summary }) => { + this.summary = summary; this.isLoading = false; + if (!this.summary) { + this.snackBarRef = this.snackBar.open( + $localize`This feature requires a subscription.`, + this.hasPermissionForSubscription + ? $localize`Upgrade Plan` + : undefined, + { duration: 6000 } + ); + + this.snackBarRef.afterDismissed().subscribe(() => { + this.snackBarRef = undefined; + }); + + this.snackBarRef.onAction().subscribe(() => { + this.router.navigate(['/pricing']); + }); + } + this.changeDetectorRef.markForCheck(); }); diff --git a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html index 577d4174..78e30675 100644 --- a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html +++ b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -172,6 +172,17 @@ > +
+
Excluded from Analysis
+
+ +
+

diff --git a/apps/client/src/app/pages/accounts/accounts-page.component.ts b/apps/client/src/app/pages/accounts/accounts-page.component.ts index 81c02c2f..6d7f8455 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.component.ts +++ b/apps/client/src/app/pages/accounts/accounts-page.component.ts @@ -59,8 +59,8 @@ export class AccountsPageComponent implements OnDestroy, OnInit { this.openCreateAccountDialog(); } else if (params['editDialog']) { if (this.accounts) { - const account = this.accounts.find((account) => { - return account.id === params['accountId']; + const account = this.accounts.find(({ id }) => { + return id === params['accountId']; }); this.openUpdateAccountDialog(account); @@ -155,6 +155,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit { balance, currency, id, + isExcluded, name, platformId }: AccountModel): void { @@ -165,6 +166,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit { balance, currency, id, + isExcluded, name, platformId } @@ -231,6 +233,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit { accountType: AccountType.SECURITIES, balance: 0, currency: this.user?.settings?.baseCurrency, + isExcluded: false, name: null, platformId: null } diff --git a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html index 97148735..ba2a1cee 100644 --- a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html +++ b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -50,6 +50,14 @@ +
+ Exclude from Analysis +
Account ID diff --git a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.module.ts b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.module.ts index 3a3f2f51..528835f9 100644 --- a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.module.ts +++ b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.module.ts @@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatDialogModule } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; @@ -15,6 +16,7 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.c CommonModule, FormsModule, MatButtonModule, + MatCheckboxModule, MatDialogModule, MatFormFieldModule, MatInputModule, diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts index d4d6abb2..032cf3b3 100644 --- a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts @@ -37,14 +37,14 @@ export class FirePageComponent implements OnDestroy, OnInit { this.deviceType = this.deviceService.getDeviceInfo().deviceType; this.dataService - .fetchPortfolioSummary() + .fetchPortfolioDetails({}) .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(({ cash, currentValue }) => { - if (cash === null || currentValue === null) { + .subscribe(({ summary }) => { + if (summary.cash === null || summary.currentValue === null) { return; } - this.fireWealth = new Big(currentValue); + this.fireWealth = new Big(summary.currentValue); this.withdrawalRatePerYear = this.fireWealth.mul(4).div(100); this.withdrawalRatePerMonth = this.withdrawalRatePerYear.div(12); diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index d33faeb7..b725a1d0 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -31,7 +31,6 @@ import { PortfolioPerformanceResponse, PortfolioPublicDetails, PortfolioReport, - PortfolioSummary, UniqueAsset, User } from '@ghostfolio/common/interfaces'; @@ -302,7 +301,11 @@ export class DataService { ); } - public fetchPortfolioDetails({ filters }: { filters?: Filter[] }) { + public fetchPortfolioDetails({ + filters + }: { + filters?: Filter[]; + }): Observable { let params = new HttpParams(); if (filters?.length > 0) { @@ -348,9 +351,20 @@ export class DataService { } } - return this.http.get('/api/v1/portfolio/details', { - params - }); + return this.http + .get('/api/v1/portfolio/details', { + params + }) + .pipe( + map((response) => { + if (response.summary?.firstOrderDate) { + response.summary.firstOrderDate = parseISO( + response.summary.firstOrderDate + ); + } + return response; + }) + ); } public fetchPortfolioPerformance({ @@ -376,18 +390,6 @@ export class DataService { return this.http.get('/api/v1/portfolio/report'); } - public fetchPortfolioSummary(): Observable { - return this.http.get('/api/v1/portfolio/summary').pipe( - map((summary) => { - if (summary.firstOrderDate) { - summary.firstOrderDate = parseISO(summary.firstOrderDate); - } - - return summary; - }) - ); - } - public fetchPositionDetail({ dataSource, symbol diff --git a/libs/common/src/lib/interfaces/portfolio-details.interface.ts b/libs/common/src/lib/interfaces/portfolio-details.interface.ts index cffa3ac0..d17e4c75 100644 --- a/libs/common/src/lib/interfaces/portfolio-details.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-details.interface.ts @@ -1,4 +1,7 @@ -import { PortfolioPosition } from '@ghostfolio/common/interfaces'; +import { + PortfolioPosition, + PortfolioSummary +} from '@ghostfolio/common/interfaces'; export interface PortfolioDetails { accounts: { @@ -13,5 +16,6 @@ export interface PortfolioDetails { filteredValueInBaseCurrency?: number; filteredValueInPercentage: number; holdings: { [symbol: string]: PortfolioPosition }; + summary: PortfolioSummary; totalValueInBaseCurrency?: number; } diff --git a/libs/common/src/lib/interfaces/portfolio-summary.interface.ts b/libs/common/src/lib/interfaces/portfolio-summary.interface.ts index b692e9ba..97a50cfe 100644 --- a/libs/common/src/lib/interfaces/portfolio-summary.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-summary.interface.ts @@ -3,9 +3,10 @@ import { PortfolioPerformance } from './portfolio-performance.interface'; export interface PortfolioSummary extends PortfolioPerformance { annualizedPerformancePercent: number; cash: number; - dividend: number; committedFunds: number; + dividend: number; emergencyFund: number; + excludedAccountsAndActivities: number; fees: number; firstOrderDate: Date; items: number; diff --git a/prisma/migrations/20220924175215_added_is_excluded_to_account/migration.sql b/prisma/migrations/20220924175215_added_is_excluded_to_account/migration.sql new file mode 100644 index 00000000..81245a47 --- /dev/null +++ b/prisma/migrations/20220924175215_added_is_excluded_to_account/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Account" ADD COLUMN "isExcluded" BOOLEAN NOT NULL DEFAULT false; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index bc7ca28f..11bef3e4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -27,6 +27,7 @@ model Account { currency String? id String @default(uuid()) isDefault Boolean @default(false) + isExcluded Boolean @default(false) name String? platformId String? updatedAt DateTime @updatedAt