diff --git a/CHANGELOG.md b/CHANGELOG.md index dc7fb9408..8726c4468 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ 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). +## 2.100.0 - 2024-08-03 + +### Added + +- Added support to manage tags of holdings in the holding detail dialog + +### Changed + +- Improved the color assignment in the chart of the holdings tab on the home page (experimental) +- Persisted the view mode of the holdings tab on the home page (experimental) +- Improved the language localization for Catalan (`ca`) +- Improved the language localization for Spanish (`es`) + ## 2.99.0 - 2024-07-29 ### Changed diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index b743eb2b7..f7be3ba00 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -46,6 +46,39 @@ export class OrderService { private readonly symbolProfileService: SymbolProfileService ) {} + public async assignTags({ + dataSource, + symbol, + tags, + userId + }: { tags: Tag[]; userId: string } & UniqueAsset) { + const orders = await this.prismaService.order.findMany({ + where: { + userId, + SymbolProfile: { + dataSource, + symbol + } + } + }); + + return Promise.all( + orders.map(({ id }) => + this.prismaService.order.update({ + data: { + tags: { + // The set operation replaces all existing connections with the provided ones + set: tags.map(({ id }) => { + return { id }; + }) + } + }, + where: { id } + }) + ) + ); + } + public async createOrder( data: Prisma.OrderCreateInput & { accountId?: string; diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 84d4ef532..3c7993c61 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -1,6 +1,7 @@ import { AccessService } from '@ghostfolio/api/app/access/access.service'; import { OrderService } from '@ghostfolio/api/app/order/order.service'; import { UserService } from '@ghostfolio/api/app/user/user.service'; +import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { hasNotDefinedValuesInObject, @@ -29,7 +30,8 @@ import { } from '@ghostfolio/common/interfaces'; import { hasReadRestrictedAccessPermission, - isRestrictedView + isRestrictedView, + permissions } from '@ghostfolio/common/permissions'; import type { DateRange, @@ -38,12 +40,14 @@ import type { } from '@ghostfolio/common/types'; import { + Body, Controller, Get, Headers, HttpException, Inject, Param, + Put, Query, UseGuards, UseInterceptors, @@ -51,12 +55,13 @@ import { } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; -import { AssetClass, AssetSubClass } from '@prisma/client'; +import { AssetClass, AssetSubClass, DataSource } from '@prisma/client'; import { Big } from 'big.js'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { PortfolioHoldingDetail } from './interfaces/portfolio-holding-detail.interface'; import { PortfolioService } from './portfolio.service'; +import { UpdateHoldingTagsDto } from './update-holding-tags.dto'; @Controller('portfolio') export class PortfolioController { @@ -566,23 +571,23 @@ export class PortfolioController { @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async getPosition( @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string, - @Param('dataSource') dataSource, - @Param('symbol') symbol + @Param('dataSource') dataSource: DataSource, + @Param('symbol') symbol: string ): Promise { - const position = await this.portfolioService.getPosition( + const holding = await this.portfolioService.getPosition( dataSource, impersonationId, symbol ); - if (position) { - return position; + if (!holding) { + throw new HttpException( + getReasonPhrase(StatusCodes.NOT_FOUND), + StatusCodes.NOT_FOUND + ); } - throw new HttpException( - getReasonPhrase(StatusCodes.NOT_FOUND), - StatusCodes.NOT_FOUND - ); + return holding; } @Get('report') @@ -605,4 +610,36 @@ export class PortfolioController { return report; } + + @HasPermission(permissions.updateOrder) + @Put('position/:dataSource/:symbol/tags') + @UseInterceptors(TransformDataSourceInRequestInterceptor) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) + public async updateHoldingTags( + @Body() data: UpdateHoldingTagsDto, + @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string, + @Param('dataSource') dataSource: DataSource, + @Param('symbol') symbol: string + ): Promise { + const holding = await this.portfolioService.getPosition( + dataSource, + impersonationId, + symbol + ); + + if (!holding) { + throw new HttpException( + getReasonPhrase(StatusCodes.NOT_FOUND), + StatusCodes.NOT_FOUND + ); + } + + await this.portfolioService.updateTags({ + dataSource, + impersonationId, + symbol, + tags: data.tags, + userId: this.request.user.id + }); + } } diff --git a/apps/api/src/app/portfolio/portfolio.service.spec.ts b/apps/api/src/app/portfolio/portfolio.service.spec.ts deleted file mode 100644 index 92970f547..000000000 --- a/apps/api/src/app/portfolio/portfolio.service.spec.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { Big } from 'big.js'; - -import { PortfolioService } from './portfolio.service'; - -describe('PortfolioService', () => { - let portfolioService: PortfolioService; - - beforeAll(async () => { - portfolioService = new PortfolioService( - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null - ); - }); - - describe('annualized performance percentage', () => { - it('Get annualized performance', async () => { - expect( - portfolioService - .getAnnualizedPerformancePercent({ - daysInMarket: NaN, // differenceInDays of date-fns returns NaN for the same day - netPerformancePercentage: new Big(0) - }) - .toNumber() - ).toEqual(0); - - expect( - portfolioService - .getAnnualizedPerformancePercent({ - daysInMarket: 0, - netPerformancePercentage: new Big(0) - }) - .toNumber() - ).toEqual(0); - - /** - * Source: https://www.readyratios.com/reference/analysis/annualized_rate.html - */ - expect( - portfolioService - .getAnnualizedPerformancePercent({ - daysInMarket: 65, // < 1 year - netPerformancePercentage: new Big(0.1025) - }) - .toNumber() - ).toBeCloseTo(0.729705); - - expect( - portfolioService - .getAnnualizedPerformancePercent({ - daysInMarket: 365, // 1 year - netPerformancePercentage: new Big(0.05) - }) - .toNumber() - ).toBeCloseTo(0.05); - - /** - * Source: https://www.investopedia.com/terms/a/annualized-total-return.asp#annualized-return-formula-and-calculation - */ - expect( - portfolioService - .getAnnualizedPerformancePercent({ - daysInMarket: 575, // > 1 year - netPerformancePercentage: new Big(0.2374) - }) - .toNumber() - ).toBeCloseTo(0.145); - }); - }); -}); diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index b5443c9cd..67529cc67 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -18,6 +18,7 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; +import { getAnnualizedPerformancePercent } from '@ghostfolio/common/calculation-helper'; import { DEFAULT_CURRENCY, EMERGENCY_FUND_TAG_ID, @@ -58,7 +59,8 @@ import { DataSource, Order, Platform, - Prisma + Prisma, + Tag } from '@prisma/client'; import { Big } from 'big.js'; import { @@ -70,7 +72,7 @@ import { parseISO, set } from 'date-fns'; -import { isEmpty, isNumber, last, uniq, uniqBy } from 'lodash'; +import { isEmpty, uniq, uniqBy } from 'lodash'; import { PortfolioCalculator } from './calculator/portfolio-calculator'; import { @@ -206,24 +208,6 @@ export class PortfolioService { }; } - public getAnnualizedPerformancePercent({ - daysInMarket, - netPerformancePercentage - }: { - daysInMarket: number; - netPerformancePercentage: Big; - }): Big { - if (isNumber(daysInMarket) && daysInMarket > 0) { - const exponent = new Big(365).div(daysInMarket).toNumber(); - - return new Big( - Math.pow(netPerformancePercentage.plus(1).toNumber(), exponent) - ).minus(1); - } - - return new Big(0); - } - public async getDividends({ activities, groupBy @@ -713,7 +697,7 @@ export class PortfolioService { return Account; }); - const dividendYieldPercent = this.getAnnualizedPerformancePercent({ + const dividendYieldPercent = getAnnualizedPerformancePercent({ daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)), netPerformancePercentage: timeWeightedInvestment.eq(0) ? new Big(0) @@ -721,7 +705,7 @@ export class PortfolioService { }); const dividendYieldPercentWithCurrencyEffect = - this.getAnnualizedPerformancePercent({ + getAnnualizedPerformancePercent({ daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)), netPerformancePercentage: timeWeightedInvestmentWithCurrencyEffect.eq( 0 @@ -1321,6 +1305,24 @@ export class PortfolioService { }; } + public async updateTags({ + dataSource, + impersonationId, + symbol, + tags, + userId + }: { + dataSource: DataSource; + impersonationId: string; + symbol: string; + tags: Tag[]; + userId: string; + }) { + userId = await this.getUserId(impersonationId, userId); + + await this.orderService.assignTags({ dataSource, symbol, tags, userId }); + } + private async getCashPositions({ cashDetails, userCurrency, @@ -1724,13 +1726,13 @@ export class PortfolioService { const daysInMarket = differenceInDays(new Date(), firstOrderDate); - const annualizedPerformancePercent = this.getAnnualizedPerformancePercent({ + const annualizedPerformancePercent = getAnnualizedPerformancePercent({ daysInMarket, netPerformancePercentage: new Big(netPerformancePercentage) })?.toNumber(); const annualizedPerformancePercentWithCurrencyEffect = - this.getAnnualizedPerformancePercent({ + getAnnualizedPerformancePercent({ daysInMarket, netPerformancePercentage: new Big( netPerformancePercentageWithCurrencyEffect diff --git a/apps/api/src/app/portfolio/update-holding-tags.dto.ts b/apps/api/src/app/portfolio/update-holding-tags.dto.ts new file mode 100644 index 000000000..11efe189d --- /dev/null +++ b/apps/api/src/app/portfolio/update-holding-tags.dto.ts @@ -0,0 +1,7 @@ +import { Tag } from '@prisma/client'; +import { IsArray } from 'class-validator'; + +export class UpdateHoldingTagsDto { + @IsArray() + tags: Tag[]; +} diff --git a/apps/api/src/app/user/update-user-setting.dto.ts b/apps/api/src/app/user/update-user-setting.dto.ts index 1fc02ff4d..78e6c27a9 100644 --- a/apps/api/src/app/user/update-user-setting.dto.ts +++ b/apps/api/src/app/user/update-user-setting.dto.ts @@ -2,6 +2,7 @@ import { IsCurrencyCode } from '@ghostfolio/api/validators/is-currency-code'; import type { ColorScheme, DateRange, + HoldingsViewMode, ViewMode } from '@ghostfolio/common/types'; @@ -66,6 +67,10 @@ export class UpdateUserSettingDto { @IsOptional() 'filters.tags'?: string[]; + @IsIn(['CHART', 'TABLE']) + @IsOptional() + holdingsViewMode?: HoldingsViewMode; + @IsBoolean() @IsOptional() isExperimentalFeatures?: boolean; diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index 02a65b6a0..7aa1dbbe8 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -190,7 +190,7 @@ export class UserService { (user.Settings.settings as UserSettings).dateRange = (user.Settings.settings as UserSettings).viewMode === 'ZEN' ? 'max' - : (user.Settings.settings as UserSettings)?.dateRange ?? 'max'; + : ((user.Settings.settings as UserSettings)?.dateRange ?? 'max'); // Set default value for view mode if (!(user.Settings.settings as UserSettings).viewMode) { @@ -243,6 +243,9 @@ export class UserService { // Reset benchmark user.Settings.settings.benchmark = undefined; + + // Reset holdings view mode + user.Settings.settings.holdingsViewMode = undefined; } else if (user.subscription?.type === 'Premium') { currentPermissions.push(permissions.reportDataGlitch); diff --git a/apps/client/src/app/app.component.ts b/apps/client/src/app/app.component.ts index 08cc915bd..4f1464408 100644 --- a/apps/client/src/app/app.component.ts +++ b/apps/client/src/app/app.component.ts @@ -259,6 +259,10 @@ export class AppComponent implements OnDestroy, OnInit { this.user?.permissions, permissions.reportDataGlitch ), + hasPermissionToUpdateOrder: + !this.hasImpersonationId && + hasPermission(this.user?.permissions, permissions.updateOrder) && + !user?.settings?.isRestrictedView, locale: this.user?.settings?.locale }, height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts index 3ed2f13a5..5673cd0c0 100644 --- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts +++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts @@ -19,16 +19,24 @@ import { GfLineChartComponent } from '@ghostfolio/ui/line-chart'; import { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart'; import { GfValueComponent } from '@ghostfolio/ui/value'; +import { COMMA, ENTER } from '@angular/cdk/keycodes'; import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, ChangeDetectionStrategy, ChangeDetectorRef, Component, + ElementRef, Inject, OnDestroy, - OnInit + OnInit, + ViewChild } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { + MatAutocompleteModule, + MatAutocompleteSelectedEvent +} from '@angular/material/autocomplete'; import { MatButtonModule } from '@angular/material/button'; import { MatChipsModule } from '@angular/material/chips'; import { @@ -36,14 +44,15 @@ import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; +import { MatFormFieldModule } from '@angular/material/form-field'; import { SortDirection } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; import { MatTabsModule } from '@angular/material/tabs'; import { Account, Tag } from '@prisma/client'; import { format, isSameMonth, isToday, parseISO } from 'date-fns'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; -import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import { Observable, of, Subject } from 'rxjs'; +import { map, startWith, takeUntil } from 'rxjs/operators'; import { HoldingDetailDialogParams } from './interfaces/interfaces'; @@ -60,9 +69,11 @@ import { HoldingDetailDialogParams } from './interfaces/interfaces'; GfLineChartComponent, GfPortfolioProportionChartComponent, GfValueComponent, + MatAutocompleteModule, MatButtonModule, MatChipsModule, MatDialogModule, + MatFormFieldModule, MatTabsModule, NgxSkeletonLoaderModule ], @@ -73,6 +84,9 @@ import { HoldingDetailDialogParams } from './interfaces/interfaces'; templateUrl: 'holding-detail-dialog.html' }) export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { + @ViewChild('tagInput') tagInput: ElementRef; + + public activityForm: FormGroup; public accounts: Account[]; public activities: Activity[]; public assetClass: string; @@ -88,6 +102,7 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { public dividendInBaseCurrencyPrecision = 2; public dividendYieldPercentWithCurrencyEffect: number; public feeInBaseCurrency: number; + public filteredTagsObservable: Observable = of([]); public firstBuyDate: string; public historicalDataItems: LineChartItem[]; public investment: number; @@ -107,10 +122,12 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { public sectors: { [name: string]: { name: string; value: number }; }; + public separatorKeysCodes: number[] = [COMMA, ENTER]; public sortColumn = 'date'; public sortDirection: SortDirection = 'desc'; public SymbolProfile: EnhancedSymbolProfile; public tags: Tag[]; + public tagsAvailable: Tag[]; public totalItems: number; public transactionCount: number; public user: User; @@ -123,10 +140,38 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { private dataService: DataService, public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: HoldingDetailDialogParams, + private formBuilder: FormBuilder, private userService: UserService ) {} public ngOnInit() { + const { tags } = this.dataService.fetchInfo(); + + this.activityForm = this.formBuilder.group({ + tags: [] + }); + + this.tagsAvailable = tags.map(({ id, name }) => { + return { + id, + name: translate(name) + }; + }); + + this.activityForm + .get('tags') + .valueChanges.pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((tags) => { + this.dataService + .putHoldingTags({ + tags, + dataSource: this.data.dataSource, + symbol: this.data.symbol + }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(); + }); + this.dataService .fetchHoldingDetail({ dataSource: this.data.dataSource, @@ -248,12 +293,27 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { this.reportDataGlitchMail = `mailto:hi@ghostfol.io?Subject=Ghostfolio Data Glitch Report&body=Hello%0D%0DI would like to report a data glitch for%0D%0DSymbol: ${SymbolProfile?.symbol}%0DData Source: ${SymbolProfile?.dataSource}%0D%0DAdditional notes:%0D%0DCan you please take a look?%0D%0DKind regards`; this.sectors = {}; this.SymbolProfile = SymbolProfile; + this.tags = tags.map(({ id, name }) => { return { id, name: translate(name) }; }); + + this.activityForm.setValue({ tags: this.tags }, { emitEvent: false }); + + this.filteredTagsObservable = this.activityForm.controls[ + 'tags' + ].valueChanges.pipe( + startWith(this.activityForm.get('tags').value), + map((aTags: Tag[] | null) => { + return aTags + ? this.filterTags(aTags) + : this.tagsAvailable.slice(); + }) + ); + this.transactionCount = transactionCount; this.totalItems = transactionCount; this.value = value; @@ -353,6 +413,17 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { }); } + public onAddTag(event: MatAutocompleteSelectedEvent) { + this.activityForm.get('tags').setValue([ + ...(this.activityForm.get('tags').value ?? []), + this.tagsAvailable.find(({ id }) => { + return id === event.option.value; + }) + ]); + + this.tagInput.nativeElement.value = ''; + } + public onClose() { this.dialogRef.close(); } @@ -377,8 +448,26 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { }); } + public onRemoveTag(aTag: Tag) { + this.activityForm.get('tags').setValue( + this.activityForm.get('tags').value.filter(({ id }) => { + return id !== aTag.id; + }) + ); + } + public ngOnDestroy() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); } + + private filterTags(aTags: Tag[]) { + const tagIds = aTags.map(({ id }) => { + return id; + }); + + return this.tagsAvailable.filter(({ id }) => { + return !tagIds.includes(id); + }); + } } diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html index 9afeef709..b7474a7a3 100644 --- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html +++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -325,7 +325,7 @@ @@ -375,7 +375,49 @@ - @if (tags?.length > 0) { +
+
+ + Tags + + @for (tag of activityForm.get('tags')?.value; track tag.id) { + + {{ tag.name }} + + + } + + + + @for (tag of filteredTagsObservable | async; track tag.id) { + + {{ tag.name }} + + } + + +
+
+ + @if (!data.hasPermissionToUpdateOrder && tagsAvailable?.length > 0) {
Tags
diff --git a/apps/client/src/app/components/holding-detail-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/holding-detail-dialog/interfaces/interfaces.ts index c6cfce1ee..8178838ab 100644 --- a/apps/client/src/app/components/holding-detail-dialog/interfaces/interfaces.ts +++ b/apps/client/src/app/components/holding-detail-dialog/interfaces/interfaces.ts @@ -9,6 +9,7 @@ export interface HoldingDetailDialogParams { deviceType: string; hasImpersonationId: boolean; hasPermissionToReportDataGlitch: boolean; + hasPermissionToUpdateOrder: boolean; locale: string; symbol: string; } diff --git a/apps/client/src/app/components/home-holdings/home-holdings.component.ts b/apps/client/src/app/components/home-holdings/home-holdings.component.ts index 3b99adb06..dca8bbe55 100644 --- a/apps/client/src/app/components/home-holdings/home-holdings.component.ts +++ b/apps/client/src/app/components/home-holdings/home-holdings.component.ts @@ -9,7 +9,7 @@ import { import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { HoldingType, - HoldingViewMode, + HoldingsViewMode, ToggleOption } from '@ghostfolio/common/types'; @@ -18,7 +18,7 @@ import { FormControl } from '@angular/forms'; import { Router } from '@angular/router'; import { DeviceDetectorService } from 'ngx-device-detector'; import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import { skip, takeUntil } from 'rxjs/operators'; @Component({ selector: 'gf-home-holdings', @@ -26,6 +26,8 @@ import { takeUntil } from 'rxjs/operators'; templateUrl: './home-holdings.html' }) export class HomeHoldingsComponent implements OnDestroy, OnInit { + public static DEFAULT_HOLDINGS_VIEW_MODE: HoldingsViewMode = 'TABLE'; + public deviceType: string; public hasImpersonationId: boolean; public hasPermissionToAccessHoldingsChart: boolean; @@ -37,7 +39,9 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { { label: $localize`Closed`, value: 'CLOSED' } ]; public user: User; - public viewModeFormControl = new FormControl('TABLE'); + public viewModeFormControl = new FormControl( + HomeHoldingsComponent.DEFAULT_HOLDINGS_VIEW_MODE + ); private unsubscribeSubject = new Subject(); @@ -81,6 +85,21 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { this.changeDetectorRef.markForCheck(); } }); + + this.viewModeFormControl.valueChanges + .pipe( + // Skip inizialization: "new FormControl" + skip(1), + takeUntil(this.unsubscribeSubject) + ) + .subscribe((holdingsViewMode) => { + this.dataService + .putUserSetting({ holdingsViewMode }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + this.userService.remove(); + }); + }); } public onChangeHoldingType(aHoldingType: HoldingType) { @@ -122,9 +141,20 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { this.hasPermissionToAccessHoldingsChart && this.holdingType === 'ACTIVE' ) { - this.viewModeFormControl.enable(); + this.viewModeFormControl.enable({ emitEvent: false }); + + this.viewModeFormControl.setValue( + this.deviceType === 'mobile' + ? HomeHoldingsComponent.DEFAULT_HOLDINGS_VIEW_MODE + : this.user?.settings?.holdingsViewMode || + HomeHoldingsComponent.DEFAULT_HOLDINGS_VIEW_MODE, + { emitEvent: false } + ); } else if (this.holdingType === 'CLOSED') { - this.viewModeFormControl.setValue('TABLE'); + this.viewModeFormControl.setValue( + HomeHoldingsComponent.DEFAULT_HOLDINGS_VIEW_MODE, + { emitEvent: false } + ); } this.holdings = undefined; diff --git a/apps/client/src/app/components/user-account-settings/user-account-settings.html b/apps/client/src/app/components/user-account-settings/user-account-settings.html index 5ec0e75a1..8b512ce3f 100644 --- a/apps/client/src/app/components/user-account-settings/user-account-settings.html +++ b/apps/client/src/app/components/user-account-settings/user-account-settings.html @@ -70,16 +70,16 @@ " > + Deutsch + English @if (user?.settings?.isExperimentalFeatures) { } - Deutsch - English @if (user?.settings?.isExperimentalFeatures) { Chinese (Community(); public Validators = Validators; @@ -81,7 +81,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { this.currencies = currencies; this.defaultDateFormat = getDateFormatString(this.locale); this.platforms = platforms; - this.tags = tags.map(({ id, name }) => { + this.tagsAvailable = tags.map(({ id, name }) => { return { id, name: translate(name) @@ -287,7 +287,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { ].valueChanges.pipe( startWith(this.activityForm.get('tags').value), map((aTags: Tag[] | null) => { - return aTags ? this.filterTags(aTags) : this.tags.slice(); + return aTags ? this.filterTags(aTags) : this.tagsAvailable.slice(); }) ); @@ -441,10 +441,11 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { public onAddTag(event: MatAutocompleteSelectedEvent) { this.activityForm.get('tags').setValue([ ...(this.activityForm.get('tags').value ?? []), - this.tags.find(({ id }) => { + this.tagsAvailable.find(({ id }) => { return id === event.option.value; }) ]); + this.tagInput.nativeElement.value = ''; } @@ -518,12 +519,12 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { } private filterTags(aTags: Tag[]) { - const tagIds = aTags.map((tag) => { - return tag.id; + const tagIds = aTags.map(({ id }) => { + return id; }); - return this.tags.filter((tag) => { - return !tagIds.includes(tag.id); + return this.tagsAvailable.filter(({ id }) => { + return !tagIds.includes(id); }); } diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html index 56cb66fcd..7795688c0 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html +++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html @@ -378,11 +378,11 @@
-
+
Tags - @for (tag of activityForm.get('tags')?.value; track tag) { + @for (tag of activityForm.get('tags')?.value; track tag.id) { - @for (tag of filteredTagsObservable | async; track tag) { + @for (tag of filteredTagsObservable | async; track tag.id) { {{ tag.name }} diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 64e498d12..4f9fd7e20 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -47,7 +47,8 @@ import { SortDirection } from '@angular/material/sort'; import { AccountBalance, DataSource, - Order as OrderModel + Order as OrderModel, + Tag } from '@prisma/client'; import { format, parseISO } from 'date-fns'; import { cloneDeep, groupBy, isNumber } from 'lodash'; @@ -649,6 +650,17 @@ export class DataService { return this.http.put(`/api/v1/admin/settings/${key}`, aData); } + public putHoldingTags({ + dataSource, + symbol, + tags + }: { tags: Tag[] } & UniqueAsset) { + return this.http.put( + `/api/v1/portfolio/position/${dataSource}/${symbol}/tags`, + { tags } + ); + } + public putOrder(aOrder: UpdateOrderDto) { return this.http.put(`/api/v1/order/${aOrder.id}`, aOrder); } diff --git a/apps/client/src/locales/messages.ca.xlf b/apps/client/src/locales/messages.ca.xlf index a423dde12..7ff136408 100644 --- a/apps/client/src/locales/messages.ca.xlf +++ b/apps/client/src/locales/messages.ca.xlf @@ -3,7 +3,7 @@ Features - Features + Característiques apps/client/src/app/app-routing.module.ts 65 @@ -11,7 +11,7 @@ Internationalization - Internationalization + Internacionalització apps/client/src/app/app-routing.module.ts 79 @@ -19,7 +19,7 @@ Sign in - Sign in + Iniciar sessió apps/client/src/app/app-routing.module.ts 141 @@ -31,7 +31,7 @@ You are using the Live Demo. - You are using the Live Demo. + Esteu utilitzant una demo. apps/client/src/app/app.component.html 12 @@ -39,7 +39,7 @@ Create Account - Create Account + Crea un Compte apps/client/src/app/app.component.html 13 @@ -55,7 +55,7 @@ Personal Finance - Personal Finance + Finances Personals apps/client/src/app/app.component.html 54 @@ -63,7 +63,7 @@ Markets - Markets + Mercats apps/client/src/app/app.component.html 58 @@ -83,7 +83,7 @@ Resources - Resources + Recursos apps/client/src/app/app.component.html 61 @@ -103,7 +103,7 @@ About - About + Sobre apps/client/src/app/app.component.html 67 @@ -119,7 +119,7 @@ Blog - Blog + Blog apps/client/src/app/app.component.html 70 @@ -207,7 +207,7 @@ Changelog - Changelog + Registre de canvis apps/client/src/app/app.component.html 74 @@ -219,7 +219,7 @@ Features - Features + Característiques apps/client/src/app/app.component.html 76 @@ -235,7 +235,7 @@ Frequently Asked Questions (FAQ) - Frequently Asked Questions (FAQ) + Preguntes Freqüents (FAQ) apps/client/src/app/app.component.html 80 @@ -247,7 +247,7 @@ License - License + Llicències apps/client/src/app/app.component.html 85 @@ -259,7 +259,7 @@ Pricing - Pricing + Preu apps/client/src/app/app.component.html 94 @@ -283,7 +283,7 @@ Privacy Policy - Privacy Policy + Política de privacitat apps/client/src/app/app.component.html 100 @@ -295,7 +295,7 @@ Community - Community + Comunitat apps/client/src/app/app.component.html 118 @@ -335,7 +335,7 @@ The risk of loss in trading can be substantial. It is not advisable to invest money you may need in the short term. - The risk of loss in trading can be substantial. It is not advisable to invest money you may need in the short term. + El risc d'assumir pèrdues en les inversions és substancial. No és recomanable invertir diners que pugui necessitar a curt termini. apps/client/src/app/app.component.html 199 @@ -343,7 +343,7 @@ about - about + sobre apps/client/src/app/app.component.ts 59 @@ -427,7 +427,7 @@ license - license + llicències apps/client/src/app/app.component.ts 61 @@ -443,7 +443,7 @@ privacy-policy - privacy-policy + política de privacitat apps/client/src/app/app.component.ts 64 @@ -459,7 +459,7 @@ faq - faq + faq apps/client/src/app/app.component.ts 66 @@ -491,7 +491,7 @@ features - features + característiques apps/client/src/app/app.component.ts 67 @@ -555,7 +555,7 @@ markets - markets + mercats apps/client/src/app/app.component.ts 68 @@ -587,7 +587,7 @@ pricing - pricing + preu apps/client/src/app/app.component.ts 69 @@ -655,7 +655,7 @@ register - register + registrar-se apps/client/src/app/app.component.ts 70 @@ -691,7 +691,7 @@ resources - resources + recursos apps/client/src/app/app.component.ts 71 @@ -743,7 +743,7 @@ Alias - Alias + Àlies apps/client/src/app/components/access-table/access-table.component.html 4 @@ -763,7 +763,7 @@ Permission - Permission + Permisos apps/client/src/app/components/access-table/access-table.component.html 18 @@ -775,7 +775,7 @@ View - View + Vista apps/client/src/app/components/access-table/access-table.component.html 23 @@ -787,7 +787,7 @@ Restricted view - Restricted view + Vista restringida apps/client/src/app/components/access-table/access-table.component.html 26 @@ -799,7 +799,7 @@ Details - Details + Detalls apps/client/src/app/components/access-table/access-table.component.html 33 @@ -807,7 +807,7 @@ Revoke - Revoke + Revocar apps/client/src/app/components/access-table/access-table.component.html 62 @@ -815,7 +815,7 @@ Do you really want to revoke this granted access? - Do you really want to revoke this granted access? + Realment vol revocar aquest accés? apps/client/src/app/components/access-table/access-table.component.ts 50 @@ -823,7 +823,7 @@ Cash Balance - Cash Balance + Balanç de Caixa apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html 45 @@ -839,7 +839,7 @@ Equity - Equity + Patrimoni apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html 56 @@ -847,7 +847,7 @@ Activities - Activities + Activitats apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html 61 @@ -887,7 +887,7 @@ Platform - Platform + Plataforma apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html 65 @@ -903,7 +903,7 @@ Holdings - Holdings + En cartera apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html 77 @@ -923,7 +923,7 @@ Cash Balances - Cash Balances + Balanç de Caixa apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html 115 @@ -931,7 +931,7 @@ Transfer Cash Balance - Transfer Cash Balance + Transferir Efectiu apps/client/src/app/components/accounts-table/accounts-table.component.html 10 @@ -943,7 +943,7 @@ Name - Name + Nom apps/client/src/app/components/accounts-table/accounts-table.component.html 43 @@ -995,7 +995,7 @@ Total - Total + Total apps/client/src/app/components/accounts-table/accounts-table.component.html 55 @@ -1003,7 +1003,7 @@ Currency - Currency + Divisa apps/client/src/app/components/accounts-table/accounts-table.component.html 65 @@ -1031,7 +1031,7 @@ Value - Value + Valor apps/client/src/app/components/accounts-table/accounts-table.component.html 171 @@ -1095,7 +1095,7 @@ Edit - Edit + Editar apps/client/src/app/components/accounts-table/accounts-table.component.html 278 @@ -1123,7 +1123,7 @@ Delete - Delete + Suprimir apps/client/src/app/components/accounts-table/accounts-table.component.html 288 @@ -1163,7 +1163,7 @@ Do you really want to delete this account? - Do you really want to delete this account? + Realment vol suprimir aquest compte? apps/client/src/app/components/accounts-table/accounts-table.component.ts 101 @@ -1171,7 +1171,7 @@ Type - Type + Tipus apps/client/src/app/components/admin-jobs/admin-jobs.html 31 @@ -1191,7 +1191,7 @@ Asset Profile - Asset Profile + Perfil d'Actiu apps/client/src/app/components/admin-jobs/admin-jobs.html 35 @@ -1199,7 +1199,7 @@ Historical Market Data - Historical Market Data + Dades Històriques de Mercat apps/client/src/app/components/admin-jobs/admin-jobs.html 37 @@ -1207,7 +1207,7 @@ Symbol - Symbol + Símbol apps/client/src/app/components/admin-jobs/admin-jobs.html 44 @@ -1231,7 +1231,7 @@ Data Source - Data Source + Origen de les Dades apps/client/src/app/components/admin-jobs/admin-jobs.html 53 @@ -1251,7 +1251,7 @@ Priority - Priority + Prioritat apps/client/src/app/components/admin-jobs/admin-jobs.html 62 @@ -1259,7 +1259,7 @@ Attempts - Attempts + Intents apps/client/src/app/components/admin-jobs/admin-jobs.html 81 @@ -1267,7 +1267,7 @@ Created - Created + Creat apps/client/src/app/components/admin-jobs/admin-jobs.html 90 @@ -1275,7 +1275,7 @@ Finished - Finished + Finalitzat apps/client/src/app/components/admin-jobs/admin-jobs.html 99 @@ -1283,7 +1283,7 @@ Status - Status + Estat apps/client/src/app/components/admin-jobs/admin-jobs.html 108 @@ -1291,7 +1291,7 @@ Delete Jobs - Delete Jobs + Aturar Processos apps/client/src/app/components/admin-jobs/admin-jobs.html 149 @@ -1299,7 +1299,7 @@ View Data - View Data + Veure les Dades apps/client/src/app/components/admin-jobs/admin-jobs.html 164 @@ -1315,7 +1315,7 @@ Execute Job - Execute Job + Executar Procés apps/client/src/app/components/admin-jobs/admin-jobs.html 174 @@ -1323,7 +1323,7 @@ Delete Job - Delete Job + Suprimir Procés apps/client/src/app/components/admin-jobs/admin-jobs.html 177 @@ -1331,7 +1331,7 @@ Details for - Details for + Detalls de apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html 2 @@ -1339,7 +1339,7 @@ Date - Date + Data apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html 6 @@ -1359,7 +1359,7 @@ Market Price - Market Price + Preu de Mercat apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html 26 @@ -1371,7 +1371,7 @@ Cancel - Cancel + Cancel·lar apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html 46 @@ -1415,7 +1415,7 @@ Save - Save + Guardar apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html 48 @@ -1451,7 +1451,7 @@ Benchmarks - Benchmarks + Punts de referència apps/client/src/app/components/admin-market-data/admin-market-data.component.ts 80 @@ -1459,7 +1459,7 @@ Currencies - Currencies + Divises apps/client/src/app/components/admin-market-data/admin-market-data.component.ts 85 @@ -1467,7 +1467,7 @@ ETFs without Countries - ETFs without Countries + ETFs sense País apps/client/src/app/components/admin-market-data/admin-market-data.component.ts 90 @@ -1475,7 +1475,7 @@ ETFs without Sectors - ETFs without Sectors + ETFs sense Sector apps/client/src/app/components/admin-market-data/admin-market-data.component.ts 95 @@ -1483,7 +1483,7 @@ Filter by... - Filter by... + Filtra per... apps/client/src/app/components/admin-market-data/admin-market-data.component.ts 322 @@ -1491,7 +1491,7 @@ Asset Class - Asset Class + Classe d'Actiu apps/client/src/app/components/admin-market-data/admin-market-data.html 86 @@ -1515,7 +1515,7 @@ Asset Sub Class - Asset Sub Class + Subclasse d'Actiu apps/client/src/app/components/admin-market-data/admin-market-data.html 95 @@ -1539,7 +1539,7 @@ First Activity - First Activity + Primera Activitat apps/client/src/app/components/admin-market-data/admin-market-data.html 104 @@ -1559,7 +1559,7 @@ Activities Count - Activities Count + Nombre d'Activitats apps/client/src/app/components/admin-market-data/admin-market-data.html 113 @@ -1567,7 +1567,7 @@ Historical Data - Historical Data + Dades Històriques apps/client/src/app/components/admin-market-data/admin-market-data.html 122 @@ -1579,7 +1579,7 @@ Sectors Count - Sectors Count + Nombre de Sectors apps/client/src/app/components/admin-market-data/admin-market-data.html 131 @@ -1587,7 +1587,7 @@ Countries Count - Countries Count + Nombre de Països apps/client/src/app/components/admin-market-data/admin-market-data.html 140 @@ -1595,7 +1595,7 @@ Gather Recent Data - Gather Recent Data + Recopilar Dades Recents apps/client/src/app/components/admin-market-data/admin-market-data.html 177 @@ -1603,7 +1603,7 @@ Gather All Data - Gather All Data + Recopilar Totes les Dades apps/client/src/app/components/admin-market-data/admin-market-data.html 180 @@ -1611,7 +1611,7 @@ Gather Profile Data - Gather Profile Data + Recopilar Dades del Perfil apps/client/src/app/components/admin-market-data/admin-market-data.html 183 @@ -1623,7 +1623,7 @@ Delete Profiles - Delete Profiles + Eliminar Perfils apps/client/src/app/components/admin-market-data/admin-market-data.html 190 @@ -1631,7 +1631,7 @@ Do you really want to delete this asset profile? - Do you really want to delete this asset profile? + Realment vol eliminar el perfil d'aquest actiu? apps/client/src/app/components/admin-market-data/admin-market-data.service.ts 18 @@ -1639,7 +1639,7 @@ Do you really want to delete these profiles? - Do you really want to delete these profiles? + Realment vol eliminar aquests perfils? apps/client/src/app/components/admin-market-data/admin-market-data.service.ts 34 @@ -1647,7 +1647,7 @@ Oops! Could not delete profiles. - Oops! Could not delete profiles. + Oooh! No s'han pogut eliminar els perfils apps/client/src/app/components/admin-market-data/admin-market-data.service.ts 45 @@ -1655,7 +1655,7 @@ Oops! Could not parse historical data. - Oops! Could not parse historical data. + Oooh! No s'han pogut recopilar les dades históriques. apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts 232 @@ -1663,7 +1663,7 @@ The current market price is - The current market price is + El preu de mercat actual és apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts 336 @@ -1671,7 +1671,7 @@ Refresh - Refresh + Refrescar apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html 22 @@ -1679,7 +1679,7 @@ Gather Historical Data - Gather Historical Data + Recopilar Dades Històriques apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html 32 @@ -1687,7 +1687,7 @@ Import - Import + Importar apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html 110 @@ -1703,7 +1703,7 @@ Sector - Sector + Sector apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html 187 @@ -1715,7 +1715,7 @@ Country - Country + País apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html 198 @@ -1731,7 +1731,7 @@ Sectors - Sectors + Sectors apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html 204 @@ -1751,7 +1751,7 @@ Countries - Countries + Països apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html 214 @@ -1767,7 +1767,7 @@ Benchmark - Benchmark + Referència apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html 286 @@ -1775,7 +1775,7 @@ Symbol Mapping - Symbol Mapping + Mapatge de Símbols apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html 292 @@ -1783,7 +1783,7 @@ Scraper Configuration - Scraper Configuration + Configuració del Proveïdor de Dades apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html 304 @@ -1791,7 +1791,7 @@ Test - Test + Prova apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html 322 @@ -1799,7 +1799,7 @@ Url - Url + Url apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html 352 @@ -1815,7 +1815,7 @@ Note - Note + Notes apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html 365 @@ -1831,7 +1831,7 @@ Add Asset Profile - Add Asset Profile + Afegeix el Perfil de l'Actiu apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html 7 @@ -1839,7 +1839,7 @@ Search - Search + Cerca apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html 16 @@ -1847,7 +1847,7 @@ Add Manually - Add Manually + Afegir manualment apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html 19 @@ -1855,7 +1855,7 @@ Name, symbol or ISIN - Name, symbol or ISIN + Nom, símbol o ISIN apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html 26 @@ -1867,7 +1867,7 @@ Please add a currency: - Please add a currency: + Si us plau, afegiu una divisa: apps/client/src/app/components/admin-overview/admin-overview.component.ts 122 @@ -1875,7 +1875,7 @@ is an invalid currency! - is an invalid currency! + no és una divisa vàlida! apps/client/src/app/components/admin-overview/admin-overview.component.ts 129 @@ -1883,7 +1883,7 @@ Do you really want to delete this coupon? - Do you really want to delete this coupon? + Està segur qeu vol eliminar aquest cupó? apps/client/src/app/components/admin-overview/admin-overview.component.ts 140 @@ -1891,7 +1891,7 @@ Do you really want to delete this currency? - Do you really want to delete this currency? + Està segur que vol eliminar aquesta divisa? apps/client/src/app/components/admin-overview/admin-overview.component.ts 153 @@ -1899,7 +1899,7 @@ Do you really want to delete this system message? - Do you really want to delete this system message? + Està segur que vol eliminar aquest missatge del sistema? apps/client/src/app/components/admin-overview/admin-overview.component.ts 166 @@ -1907,7 +1907,7 @@ Do you really want to flush the cache? - Do you really want to flush the cache? + Està segur que vol depurar el cache? apps/client/src/app/components/admin-overview/admin-overview.component.ts 183 @@ -1915,7 +1915,7 @@ Please set your system message: - Please set your system message: + Si us plau, afegeixi el seu missatge del sistema: apps/client/src/app/components/admin-overview/admin-overview.component.ts 214 @@ -1923,7 +1923,7 @@ Version - Version + Versió apps/client/src/app/components/admin-overview/admin-overview.html 7 @@ -1931,7 +1931,7 @@ User Count - User Count + Número d'Usuaris apps/client/src/app/components/admin-overview/admin-overview.html 13 @@ -1939,7 +1939,7 @@ Activity Count - Activity Count + Número d'Activitats apps/client/src/app/components/admin-overview/admin-overview.html 23 @@ -1947,7 +1947,7 @@ per User - per User + per Usuari apps/client/src/app/components/admin-overview/admin-overview.html 33 @@ -1955,7 +1955,7 @@ Exchange Rates - Exchange Rates + Tipus de Canvi apps/client/src/app/components/admin-overview/admin-overview.html 39 @@ -1963,7 +1963,7 @@ Add Currency - Add Currency + Afegir Divisa apps/client/src/app/components/admin-overview/admin-overview.html 109 @@ -1971,7 +1971,7 @@ User Signup - User Signup + Registrar Usuari apps/client/src/app/components/admin-overview/admin-overview.html 115 @@ -1979,7 +1979,7 @@ Read-only Mode - Read-only Mode + Mode Només Lecutra apps/client/src/app/components/admin-overview/admin-overview.html 129 @@ -1987,7 +1987,7 @@ Data Gathering - Data Gathering + Recollida de Dades apps/client/src/app/components/admin-overview/admin-overview.html 141 @@ -1995,7 +1995,7 @@ System Message - System Message + Missatge del Sistema apps/client/src/app/components/admin-overview/admin-overview.html 153 @@ -2003,7 +2003,7 @@ Set Message - Set Message + Estableix el Missatge apps/client/src/app/components/admin-overview/admin-overview.html 175 @@ -2011,7 +2011,7 @@ Coupons - Coupons + Coupons apps/client/src/app/components/admin-overview/admin-overview.html 183 @@ -2019,7 +2019,7 @@ Add - Add + Afegir apps/client/src/app/components/admin-overview/admin-overview.html 243 @@ -2031,7 +2031,7 @@ Housekeeping - Housekeeping + Ordre apps/client/src/app/components/admin-overview/admin-overview.html 251 @@ -2039,7 +2039,7 @@ Flush Cache - Flush Cache + Depurar el Cache apps/client/src/app/components/admin-overview/admin-overview.html 255 @@ -2047,7 +2047,7 @@ Add Platform - Add Platform + Afegeix Plataforma apps/client/src/app/components/admin-platform/admin-platform.component.html 11 @@ -2055,7 +2055,7 @@ Accounts - Accounts + Comptes apps/client/src/app/components/admin-platform/admin-platform.component.html 65 @@ -2087,7 +2087,7 @@ Do you really want to delete this platform? - Do you really want to delete this platform? + Està segur que vol eliminar aquesta plataforma? apps/client/src/app/components/admin-platform/admin-platform.component.ts 79 @@ -2095,7 +2095,7 @@ Update platform - Update platform + Actualitzar plataforma apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html 8 @@ -2103,7 +2103,7 @@ Add platform - Add platform + Afegeix una plataforma apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html 10 @@ -2111,7 +2111,7 @@ Platforms - Platforms + Plataformes apps/client/src/app/components/admin-settings/admin-settings.component.html 4 @@ -2119,7 +2119,7 @@ Tags - Tags + Etiquetes apps/client/src/app/components/admin-settings/admin-settings.component.html 10 @@ -2139,7 +2139,7 @@ Add Tag - Add Tag + Afegir Etiqueta apps/client/src/app/components/admin-tag/admin-tag.component.html 11 @@ -2147,7 +2147,7 @@ Do you really want to delete this tag? - Do you really want to delete this tag? + Està segur que vol eliminar aquesta etiqueta? apps/client/src/app/components/admin-tag/admin-tag.component.ts 79 @@ -2155,7 +2155,7 @@ Update tag - Update tag + Actualitzar etiqueta apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.html 8 @@ -2163,7 +2163,7 @@ Add tag - Add tag + Afegir etiqueta apps/client/src/app/components/admin-tag/create-or-update-tag-dialog/create-or-update-tag-dialog.html 10 @@ -2171,7 +2171,7 @@ Do you really want to delete this user? - Do you really want to delete this user? + Està segur que vol eliminar aquest usuari? apps/client/src/app/components/admin-users/admin-users.component.ts 113 @@ -2179,7 +2179,7 @@ User - User + Usuari apps/client/src/app/components/admin-users/admin-users.html 29 @@ -2187,7 +2187,7 @@ Registration - Registration + Registrar-se apps/client/src/app/components/admin-users/admin-users.html 97 @@ -2195,7 +2195,7 @@ Engagement per Day - Engagement per Day + Implicació per Dia apps/client/src/app/components/admin-users/admin-users.html 157 @@ -2203,7 +2203,7 @@ Last Request - Last Request + Última Solicitut apps/client/src/app/components/admin-users/admin-users.html 181 @@ -2211,7 +2211,7 @@ Impersonate User - Impersonate User + Actuar com un altre Usuari apps/client/src/app/components/admin-users/admin-users.html 218 @@ -2219,7 +2219,7 @@ Delete User - Delete User + Eliminar Usuari apps/client/src/app/components/admin-users/admin-users.html 229 @@ -2227,7 +2227,7 @@ Performance - Performance + Rendiment apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html 6 @@ -2243,7 +2243,7 @@ Compare with... - Compare with... + Comparar amb... apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html 18 @@ -2251,7 +2251,7 @@ Manage Benchmarks - Manage Benchmarks + Gestionar els Punts de Referència apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html 35 @@ -2259,7 +2259,7 @@ Portfolio - Portfolio + Portfolio apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts 116 @@ -2271,7 +2271,7 @@ Benchmark - Benchmark + Punt de Referència apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts 128 @@ -2279,7 +2279,7 @@ Current Market Mood - Current Market Mood + Sentiment del Mercat apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.html 12 @@ -2287,7 +2287,7 @@ Overview - Overview + Visió General apps/client/src/app/components/header/header.component.html 28 @@ -2299,7 +2299,7 @@ Portfolio - Portfolio + Portfolio apps/client/src/app/components/header/header.component.html 41 @@ -2311,7 +2311,7 @@ Admin Control - Admin Control + Panell d'Administració apps/client/src/app/components/header/header.component.html 68 @@ -2323,7 +2323,7 @@ Upgrade Plan - Upgrade Plan + Millora la teva Subscripció apps/client/src/app/components/header/header.component.html 178 @@ -2343,7 +2343,7 @@ Renew Plan - Renew Plan + Renova la teva Subscripció apps/client/src/app/components/header/header.component.html 183 @@ -2359,7 +2359,7 @@ Me - Me + Tu apps/client/src/app/components/header/header.component.html 203 @@ -2367,7 +2367,7 @@ User - User + Usuari apps/client/src/app/components/header/header.component.html 221 @@ -2375,7 +2375,7 @@ My Ghostfolio - My Ghostfolio + El meu Ghostfolio apps/client/src/app/components/header/header.component.html 262 @@ -2383,7 +2383,7 @@ About Ghostfolio - About Ghostfolio + Sobre Ghostfolio apps/client/src/app/components/header/header.component.html 303 @@ -2395,7 +2395,7 @@ Sign in - Sign in + Iniciar Sessió apps/client/src/app/components/header/header.component.html 394 @@ -2407,7 +2407,7 @@ Get started - Get started + Primers Passos apps/client/src/app/components/header/header.component.html 404 @@ -2415,7 +2415,7 @@ Oops! Incorrect Security Token. - Oops! Incorrect Security Token. + Oooh! El testimoni de seguretat és incorrecte. apps/client/src/app/components/header/header.component.ts 243 @@ -2427,7 +2427,7 @@ Change with currency effect - Change with currency effect + Canvia amb els efectes de la divisa apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html 53 @@ -2435,7 +2435,7 @@ Change - Change + Canvia apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html 65 @@ -2447,7 +2447,7 @@ Performance with currency effect - Performance with currency effect + Rendiment amb els efectes de la divisa apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html 81 @@ -2455,7 +2455,7 @@ Average Unit Price - Average Unit Price + Preu Mig per Unitat apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html 103 @@ -2463,7 +2463,7 @@ Minimum Price - Minimum Price + Preu Mínim apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html 130 @@ -2471,7 +2471,7 @@ Maximum Price - Maximum Price + Preu Màxim apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html 146 @@ -2479,7 +2479,7 @@ Quantity - Quantity + Quantitat apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html 156 @@ -2495,7 +2495,7 @@ Investment - Investment + Inversió apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html 168 @@ -2507,7 +2507,7 @@ Dividend - Dividend + Dividend apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html 181 @@ -2531,7 +2531,7 @@ Dividend Yield - Dividend Yield + Rendiment del Dividend apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html 191 @@ -2539,7 +2539,7 @@ Fees - Fees + Comissions apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html 203 @@ -2555,7 +2555,7 @@ Activity - Activity + Activitat apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html 223 @@ -2563,7 +2563,7 @@ Report Data Glitch - Report Data Glitch + Informar d'un Problema amb les Dades apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html 399 @@ -2571,7 +2571,7 @@ Active - Active + en Actiiu apps/client/src/app/components/home-holdings/home-holdings.component.ts 36 @@ -2579,7 +2579,7 @@ Closed - Closed + Finalitzat apps/client/src/app/components/home-holdings/home-holdings.component.ts 37 @@ -2587,7 +2587,7 @@ Table - Table + Taula apps/client/src/app/components/home-holdings/home-holdings.html 17 @@ -2595,7 +2595,7 @@ Chart - Chart + Gràfic apps/client/src/app/components/home-holdings/home-holdings.html 20 @@ -2603,7 +2603,7 @@ Manage Activities - Manage Activities + Gestionar Activitats apps/client/src/app/components/home-holdings/home-holdings.html 60 @@ -2611,7 +2611,7 @@ Fear - Fear + Por apps/client/src/app/components/home-market/home-market.component.ts 27 @@ -2623,7 +2623,7 @@ Greed - Greed + Cobdícia apps/client/src/app/components/home-market/home-market.component.ts 28 @@ -2635,7 +2635,7 @@ Last Days - Last Days + Últims Dies apps/client/src/app/components/home-market/home-market.html 7 @@ -2643,7 +2643,7 @@ Welcome to Ghostfolio - Welcome to Ghostfolio + Benvingut a Ghostfolio apps/client/src/app/components/home-overview/home-overview.html 7 @@ -2651,7 +2651,7 @@ Ready to take control of your personal finances? - Ready to take control of your personal finances? + Estàs preparat per controlar les teves finances personals? apps/client/src/app/components/home-overview/home-overview.html 8 @@ -2659,7 +2659,7 @@ Setup your accounts - Setup your accounts + Configura els teus comptes apps/client/src/app/components/home-overview/home-overview.html 15 @@ -6475,7 +6475,7 @@ Equity - Equity + Patrimoni libs/ui/src/lib/i18n.ts 42 @@ -6679,4 +6679,4 @@ - \ No newline at end of file + diff --git a/apps/client/src/locales/messages.es.xlf b/apps/client/src/locales/messages.es.xlf index 284876006..a2736f959 100644 --- a/apps/client/src/locales/messages.es.xlf +++ b/apps/client/src/locales/messages.es.xlf @@ -3652,7 +3652,7 @@ Fully managed Ghostfolio cloud offering. - Fully managed Ghostfolio cloud offering. + Oferta en la nube de Ghostfolio totalmente administrada. apps/client/src/app/pages/pricing/pricing-page.html 152 @@ -3664,7 +3664,7 @@ For ambitious investors who need the full picture of their financial assets. - For ambitious investors who need the full picture of their financial assets. + Para inversores ambiciosos que necesitan una visión completa de sus activos financieros apps/client/src/app/pages/pricing/pricing-page.html 184 @@ -3672,7 +3672,7 @@ One-time payment, no auto-renewal. - One-time payment, no auto-renewal. + Pago único, sin renovación automática. apps/client/src/app/pages/pricing/pricing-page.html 280 @@ -3780,7 +3780,7 @@ Switch to Ghostfolio Premium or Ghostfolio Open Source easily - Switch to Ghostfolio Premium or Ghostfolio Open Source easily + Cambie a Ghostfolio Premium o Ghostfolio Open Source fácilmente libs/ui/src/lib/i18n.ts 10 @@ -3788,7 +3788,7 @@ Switch to Ghostfolio Open Source or Ghostfolio Basic easily - Switch to Ghostfolio Open Source or Ghostfolio Basic easily + Cambie a Ghostfolio Open Source o Ghostfolio Basic fácilmente libs/ui/src/lib/i18n.ts 12 @@ -3796,7 +3796,7 @@ Market data provided by - Market data provided by + Datos de mercado proporcionados por libs/ui/src/lib/data-provider-credits/data-provider-credits.component.html 2 diff --git a/apps/client/src/styles/theme.scss b/apps/client/src/styles/theme.scss index bc4d7b73d..40a872c72 100644 --- a/apps/client/src/styles/theme.scss +++ b/apps/client/src/styles/theme.scss @@ -73,40 +73,40 @@ $gf-secondary: ( $gf-typography: mat.m2-define-typography-config(); -@include mat.core(); - // Create default theme $gf-theme-default: mat.m2-define-light-theme( ( color: ( - primary: mat.m2-define-palette($gf-primary), - accent: mat.m2-define-palette($gf-secondary, 500, 900, A100) + accent: mat.m2-define-palette($gf-secondary, 500, 900, A100), + primary: mat.m2-define-palette($gf-primary) ), density: -3, typography: $gf-typography ) ); + @include mat.all-component-themes($gf-theme-default); -@include mat.button-density(0); -@include mat.table-density(-1); // Create dark theme $gf-theme-dark: mat.m2-define-dark-theme( ( color: ( - primary: mat.m2-define-palette($gf-primary), - accent: mat.m2-define-palette($gf-secondary, 500, 900, A100) + accent: mat.m2-define-palette($gf-secondary, 500, 900, A100), + primary: mat.m2-define-palette($gf-primary) ), density: -3, typography: $gf-typography ) ); + .is-dark-theme { @include mat.all-component-colors($gf-theme-dark); - @include mat.button-density(0); - @include mat.table-density(-1); } +@include mat.button-density(0); +@include mat.core(); +@include mat.table-density(-1); + :root { --gf-theme-alpha-hover: 0.04; --gf-theme-primary-500: #36cfcc; diff --git a/libs/common/src/lib/calculation-helper.spec.ts b/libs/common/src/lib/calculation-helper.spec.ts new file mode 100644 index 000000000..69621ec0a --- /dev/null +++ b/libs/common/src/lib/calculation-helper.spec.ts @@ -0,0 +1,50 @@ +import { Big } from 'big.js'; + +import { getAnnualizedPerformancePercent } from './calculation-helper'; + +describe('CalculationHelper', () => { + describe('annualized performance percentage', () => { + it('Get annualized performance', async () => { + expect( + getAnnualizedPerformancePercent({ + daysInMarket: NaN, // differenceInDays of date-fns returns NaN for the same day + netPerformancePercentage: new Big(0) + }).toNumber() + ).toEqual(0); + + expect( + getAnnualizedPerformancePercent({ + daysInMarket: 0, + netPerformancePercentage: new Big(0) + }).toNumber() + ).toEqual(0); + + /** + * Source: https://www.readyratios.com/reference/analysis/annualized_rate.html + */ + expect( + getAnnualizedPerformancePercent({ + daysInMarket: 65, // < 1 year + netPerformancePercentage: new Big(0.1025) + }).toNumber() + ).toBeCloseTo(0.729705); + + expect( + getAnnualizedPerformancePercent({ + daysInMarket: 365, // 1 year + netPerformancePercentage: new Big(0.05) + }).toNumber() + ).toBeCloseTo(0.05); + + /** + * Source: https://www.investopedia.com/terms/a/annualized-total-return.asp#annualized-return-formula-and-calculation + */ + expect( + getAnnualizedPerformancePercent({ + daysInMarket: 575, // > 1 year + netPerformancePercentage: new Big(0.2374) + }).toNumber() + ).toBeCloseTo(0.145); + }); + }); +}); diff --git a/libs/common/src/lib/calculation-helper.ts b/libs/common/src/lib/calculation-helper.ts new file mode 100644 index 000000000..7d2ec9099 --- /dev/null +++ b/libs/common/src/lib/calculation-helper.ts @@ -0,0 +1,20 @@ +import { Big } from 'big.js'; +import { isNumber } from 'lodash'; + +export function getAnnualizedPerformancePercent({ + daysInMarket, + netPerformancePercentage +}: { + daysInMarket: number; + netPerformancePercentage: Big; +}): Big { + if (isNumber(daysInMarket) && daysInMarket > 0) { + const exponent = new Big(365).div(daysInMarket).toNumber(); + + return new Big( + Math.pow(netPerformancePercentage.plus(1).toNumber(), exponent) + ).minus(1); + } + + return new Big(0); +} diff --git a/libs/common/src/lib/interfaces/user-settings.interface.ts b/libs/common/src/lib/interfaces/user-settings.interface.ts index a0599d132..5c88e3f4b 100644 --- a/libs/common/src/lib/interfaces/user-settings.interface.ts +++ b/libs/common/src/lib/interfaces/user-settings.interface.ts @@ -1,4 +1,9 @@ -import { ColorScheme, DateRange, ViewMode } from '@ghostfolio/common/types'; +import { + ColorScheme, + DateRange, + HoldingsViewMode, + ViewMode +} from '@ghostfolio/common/types'; export interface UserSettings { annualInterestRate?: number; @@ -9,6 +14,7 @@ export interface UserSettings { emergencyFund?: number; 'filters.accounts'?: string[]; 'filters.tags'?: string[]; + holdingsViewMode?: HoldingsViewMode; isExperimentalFeatures?: boolean; isRestrictedView?: boolean; language?: string; diff --git a/libs/common/src/lib/types/holding-view-mode.type.ts b/libs/common/src/lib/types/holding-view-mode.type.ts deleted file mode 100644 index 50a4e2b29..000000000 --- a/libs/common/src/lib/types/holding-view-mode.type.ts +++ /dev/null @@ -1 +0,0 @@ -export type HoldingViewMode = 'CHART' | 'TABLE'; diff --git a/libs/common/src/lib/types/holdings-view-mode.type.ts b/libs/common/src/lib/types/holdings-view-mode.type.ts new file mode 100644 index 000000000..7b5d0a09c --- /dev/null +++ b/libs/common/src/lib/types/holdings-view-mode.type.ts @@ -0,0 +1 @@ +export type HoldingsViewMode = 'CHART' | 'TABLE'; diff --git a/libs/common/src/lib/types/index.ts b/libs/common/src/lib/types/index.ts index 65fdfe5f0..68d4a2ec4 100644 --- a/libs/common/src/lib/types/index.ts +++ b/libs/common/src/lib/types/index.ts @@ -8,7 +8,7 @@ import type { DateRange } from './date-range.type'; import type { Granularity } from './granularity.type'; import type { GroupBy } from './group-by.type'; import type { HoldingType } from './holding-type.type'; -import type { HoldingViewMode } from './holding-view-mode.type'; +import type { HoldingsViewMode } from './holdings-view-mode.type'; import type { MarketAdvanced } from './market-advanced.type'; import type { MarketDataPreset } from './market-data-preset.type'; import type { MarketState } from './market-state.type'; @@ -31,7 +31,7 @@ export type { Granularity, GroupBy, HoldingType, - HoldingViewMode, + HoldingsViewMode, Market, MarketAdvanced, MarketDataPreset, diff --git a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts index 9ee6a7aeb..3c1b3d540 100644 --- a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts +++ b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts @@ -1,3 +1,4 @@ +import { getAnnualizedPerformancePercent } from '@ghostfolio/common/calculation-helper'; import { PortfolioPosition, UniqueAsset } from '@ghostfolio/common/interfaces'; import { CommonModule } from '@angular/common'; @@ -14,10 +15,12 @@ import { ViewChild } from '@angular/core'; import { DataSource } from '@prisma/client'; +import { Big } from 'big.js'; import { ChartConfiguration } from 'chart.js'; import { LinearScale } from 'chart.js'; import { Chart } from 'chart.js'; import { TreemapController, TreemapElement } from 'chartjs-chart-treemap'; +import { differenceInDays } from 'date-fns'; import { orderBy } from 'lodash'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; @@ -41,6 +44,8 @@ export class GfTreemapChartComponent @ViewChild('chartCanvas') chartCanvas: ElementRef; + public static readonly HEAT_MULTIPLIER = 5; + public chart: Chart<'treemap'>; public isLoading = true; @@ -71,24 +76,52 @@ export class GfTreemapChartComponent datasets: [ { backgroundColor(ctx) { - const netPerformancePercentWithCurrencyEffect = - ctx.raw._data.netPerformancePercentWithCurrencyEffect; - - if (netPerformancePercentWithCurrencyEffect > 0.03) { + const annualizedNetPerformancePercentWithCurrencyEffect = + getAnnualizedPerformancePercent({ + daysInMarket: differenceInDays( + new Date(), + ctx.raw._data.dateOfFirstActivity + ), + netPerformancePercentage: new Big( + ctx.raw._data.netPerformancePercentWithCurrencyEffect + ) + }).toNumber(); + + if ( + annualizedNetPerformancePercentWithCurrencyEffect > + 0.03 * GfTreemapChartComponent.HEAT_MULTIPLIER + ) { return green[9]; - } else if (netPerformancePercentWithCurrencyEffect > 0.02) { + } else if ( + annualizedNetPerformancePercentWithCurrencyEffect > + 0.02 * GfTreemapChartComponent.HEAT_MULTIPLIER + ) { return green[7]; - } else if (netPerformancePercentWithCurrencyEffect > 0.01) { + } else if ( + annualizedNetPerformancePercentWithCurrencyEffect > + 0.01 * GfTreemapChartComponent.HEAT_MULTIPLIER + ) { return green[5]; - } else if (netPerformancePercentWithCurrencyEffect > 0) { + } else if (annualizedNetPerformancePercentWithCurrencyEffect > 0) { return green[3]; - } else if (netPerformancePercentWithCurrencyEffect === 0) { + } else if ( + annualizedNetPerformancePercentWithCurrencyEffect === 0 + ) { return gray[3]; - } else if (netPerformancePercentWithCurrencyEffect > -0.01) { + } else if ( + annualizedNetPerformancePercentWithCurrencyEffect > + -0.01 * GfTreemapChartComponent.HEAT_MULTIPLIER + ) { return red[3]; - } else if (netPerformancePercentWithCurrencyEffect > -0.02) { + } else if ( + annualizedNetPerformancePercentWithCurrencyEffect > + -0.02 * GfTreemapChartComponent.HEAT_MULTIPLIER + ) { return red[5]; - } else if (netPerformancePercentWithCurrencyEffect > -0.03) { + } else if ( + annualizedNetPerformancePercentWithCurrencyEffect > + -0.03 * GfTreemapChartComponent.HEAT_MULTIPLIER + ) { return red[7]; } else { return red[9]; diff --git a/package.json b/package.json index 9d4e673d9..c84514f6a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "2.99.0", + "version": "2.100.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio",