From ca0fb33176b496bfc8fcdc3ab1667b9b1a45269b Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 24 Mar 2024 11:59:15 +0100 Subject: [PATCH] Initial setup --- .../src/app/portfolio/portfolio.service.ts | 33 +++++---- .../src/app/user/update-user-setting.dto.ts | 16 ++++- apps/api/src/app/user/user.service.ts | 26 ++++--- .../src/app/services/user/user.service.ts | 4 ++ .../src/lib/interfaces/user.interface.ts | 1 + libs/common/src/lib/types/date-range.type.ts | 10 ++- .../src/lib/assistant/assistant.component.ts | 67 +++++++++++++------ .../lib/assistant/interfaces/interfaces.ts | 6 ++ 8 files changed, 116 insertions(+), 47 deletions(-) diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 7fe82504f..c2a0ebe29 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -1178,6 +1178,7 @@ export class PortfolioService { }) ); + // TODO: Refactor to getInterval()? const startDate = this.getStartDate(dateRange, portfolioStart); const { currentValueInBaseCurrency, @@ -1192,7 +1193,7 @@ export class PortfolioService { netPerformancePercentageWithCurrencyEffect, netPerformanceWithCurrencyEffect, totalInvestment - } = await portfolioCalculator.getCurrentPositions(startDate); + } = await portfolioCalculator.getCurrentPositions(startDate); // TODO: Provide endDate let currentNetPerformance = netPerformance; @@ -1452,6 +1453,8 @@ export class PortfolioService { dateRange, portfolioCalculator.getStartDate() ); + + // TODO const endDate = new Date(); const daysInMarket = differenceInDays(endDate, startDate) + 1; const step = withDataDecimation @@ -1618,22 +1621,24 @@ export class PortfolioService { } private getStartDate(aDateRange: DateRange, portfolioStart: Date) { + let startDate = portfolioStart; + switch (aDateRange) { case '1d': - portfolioStart = max([ - portfolioStart, + startDate = max([ + startDate, subDays(new Date().setHours(0, 0, 0, 0), 1) ]); break; case 'mtd': - portfolioStart = max([ - portfolioStart, + startDate = max([ + startDate, subDays(startOfMonth(new Date().setHours(0, 0, 0, 0)), 1) ]); break; case 'wtd': - portfolioStart = max([ - portfolioStart, + startDate = max([ + startDate, subDays( startOfWeek(new Date().setHours(0, 0, 0, 0), { weekStartsOn: 1 }), 1 @@ -1641,26 +1646,26 @@ export class PortfolioService { ]); break; case 'ytd': - portfolioStart = max([ - portfolioStart, + startDate = max([ + startDate, subDays(startOfYear(new Date().setHours(0, 0, 0, 0)), 1) ]); break; case '1y': - portfolioStart = max([ - portfolioStart, + startDate = max([ + startDate, subYears(new Date().setHours(0, 0, 0, 0), 1) ]); break; case '5y': - portfolioStart = max([ - portfolioStart, + startDate = max([ + startDate, subYears(new Date().setHours(0, 0, 0, 0), 5) ]); break; } - return portfolioStart; + return startDate; } private getStreaks({ 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 7b09ced10..2e86f4622 100644 --- a/apps/api/src/app/user/update-user-setting.dto.ts +++ b/apps/api/src/app/user/update-user-setting.dto.ts @@ -14,6 +14,7 @@ import { IsOptional, IsString } from 'class-validator'; +import { eachYearOfInterval, format } from 'date-fns'; export class UpdateUserSettingDto { @IsNumber() @@ -32,7 +33,20 @@ export class UpdateUserSettingDto { @IsOptional() colorScheme?: ColorScheme; - @IsIn(['1d', '1y', '5y', 'max', 'mtd', 'wtd', 'ytd']) + @IsIn([ + '1d', + '1y', + '5y', + 'max', + 'mtd', + 'wtd', + 'ytd', + ...eachYearOfInterval({ end: new Date(1900), start: new Date() }).map( + (date) => { + return format(date, 'yyyy'); + } + ) + ]) @IsOptional() dateRange?: DateRange; diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index 8b7c88560..4cc60770f 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -51,13 +51,22 @@ export class UserService { { Account, id, permissions, Settings, subscription }: UserWithSettings, aLocale = locale ): Promise { - const access = await this.prismaService.access.findMany({ - include: { - User: true - }, - orderBy: { alias: 'asc' }, - where: { GranteeUser: { id } } - }); + let [access, firstActivity, tags] = await Promise.all([ + this.prismaService.access.findMany({ + include: { + User: true + }, + orderBy: { alias: 'asc' }, + where: { GranteeUser: { id } } + }), + this.prismaService.order.findFirst({ + orderBy: { + date: 'asc' + }, + where: { userId: id } + }), + this.tagService.getByUser(id) + ]); let systemMessage: SystemMessage; @@ -69,8 +78,6 @@ export class UserService { systemMessage = systemMessageProperty; } - let tags = await this.tagService.getByUser(id); - if ( this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && subscription.type === 'Basic' @@ -91,6 +98,7 @@ export class UserService { }; }), accounts: Account, + dateOfFirstActivity: firstActivity?.date ?? new Date(), settings: { ...(Settings.settings), locale: (Settings.settings)?.locale ?? aLocale diff --git a/apps/client/src/app/services/user/user.service.ts b/apps/client/src/app/services/user/user.service.ts index d8cb63d0b..b1c98bde7 100644 --- a/apps/client/src/app/services/user/user.service.ts +++ b/apps/client/src/app/services/user/user.service.ts @@ -82,6 +82,10 @@ export class UserService extends ObservableStore { private fetchUser(): Observable { return this.http.get('/api/v1/user').pipe( map((user) => { + if (user.dateOfFirstActivity) { + user.dateOfFirstActivity = parseISO(user.dateOfFirstActivity); + } + if (user.settings?.retirementDate) { user.settings.retirementDate = parseISO(user.settings.retirementDate); } diff --git a/libs/common/src/lib/interfaces/user.interface.ts b/libs/common/src/lib/interfaces/user.interface.ts index 16893de6d..2891314a0 100644 --- a/libs/common/src/lib/interfaces/user.interface.ts +++ b/libs/common/src/lib/interfaces/user.interface.ts @@ -13,6 +13,7 @@ export interface User { id: string; }[]; accounts: Account[]; + dateOfFirstActivity: Date; id: string; permissions: string[]; settings: UserSettings; diff --git a/libs/common/src/lib/types/date-range.type.ts b/libs/common/src/lib/types/date-range.type.ts index 41aa877de..09fa3c15b 100644 --- a/libs/common/src/lib/types/date-range.type.ts +++ b/libs/common/src/lib/types/date-range.type.ts @@ -1 +1,9 @@ -export type DateRange = '1d' | '1y' | '5y' | 'max' | 'mtd' | 'wtd' | 'ytd'; +export type DateRange = + | '1d' + | '1y' + | '5y' + | 'max' + | 'mtd' + | 'wtd' + | 'ytd' + | string; // '2024', '2023', '2022', etc. diff --git a/libs/ui/src/lib/assistant/assistant.component.ts b/libs/ui/src/lib/assistant/assistant.component.ts index f4f9beea1..bd8355125 100644 --- a/libs/ui/src/lib/assistant/assistant.component.ts +++ b/libs/ui/src/lib/assistant/assistant.component.ts @@ -24,6 +24,7 @@ import { import { FormBuilder, FormControl } from '@angular/forms'; import { MatMenuTrigger } from '@angular/material/menu'; import { Account, AssetClass } from '@prisma/client'; +import { eachYearOfInterval, format } from 'date-fns'; import { EMPTY, Observable, Subject, lastValueFrom } from 'rxjs'; import { catchError, @@ -35,7 +36,11 @@ import { } from 'rxjs/operators'; import { AssistantListItemComponent } from './assistant-list-item/assistant-list-item.component'; -import { ISearchResultItem, ISearchResults } from './interfaces/interfaces'; +import { + IDateRangeOption, + ISearchResultItem, + ISearchResults +} from './interfaces/interfaces'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, @@ -95,27 +100,7 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit { public accounts: Account[] = []; public assetClasses: Filter[] = []; public dateRangeFormControl = new FormControl(undefined); - public readonly dateRangeOptions = [ - { label: $localize`Today`, value: '1d' }, - { - label: $localize`Week to date` + ' (' + $localize`WTD` + ')', - value: 'wtd' - }, - { - label: $localize`Month to date` + ' (' + $localize`MTD` + ')', - value: 'mtd' - }, - { - label: $localize`Year to date` + ' (' + $localize`YTD` + ')', - value: 'ytd' - }, - { label: '1 ' + $localize`year` + ' (' + $localize`1Y` + ')', value: '1y' }, - { - label: '5 ' + $localize`years` + ' (' + $localize`5Y` + ')', - value: '5y' - }, - { label: $localize`Max`, value: 'max' } - ]; + public dateRangeOptions: IDateRangeOption[] = []; public filterForm = this.formBuilder.group({ account: new FormControl(undefined), assetClass: new FormControl(undefined), @@ -199,6 +184,44 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit { } public ngOnChanges() { + this.dateRangeOptions = [ + { label: $localize`Today`, value: '1d' }, + { + label: $localize`Week to date` + ' (' + $localize`WTD` + ')', + value: 'wtd' + }, + { + label: $localize`Month to date` + ' (' + $localize`MTD` + ')', + value: 'mtd' + }, + { + label: $localize`Year to date` + ' (' + $localize`YTD` + ')', + value: 'ytd' + }, + { + label: '1 ' + $localize`year` + ' (' + $localize`1Y` + ')', + value: '1y' + }, + { + label: '5 ' + $localize`years` + ' (' + $localize`5Y` + ')', + value: '5y' + }, + { label: $localize`Max`, value: 'max' } + ]; + + if (this.user?.settings?.isExperimentalFeatures) { + this.dateRangeOptions = this.dateRangeOptions.concat( + eachYearOfInterval({ + end: new Date(), + start: this.user?.dateOfFirstActivity ?? new Date() + }) + .map((date) => { + return { label: format(date, 'yyyy'), value: format(date, 'yyyy') }; + }) + .slice(0, -1) + ); + } + this.dateRangeFormControl.setValue(this.user?.settings?.dateRange ?? null); this.filterForm.setValue( diff --git a/libs/ui/src/lib/assistant/interfaces/interfaces.ts b/libs/ui/src/lib/assistant/interfaces/interfaces.ts index 99f70dbe1..2597ccef0 100644 --- a/libs/ui/src/lib/assistant/interfaces/interfaces.ts +++ b/libs/ui/src/lib/assistant/interfaces/interfaces.ts @@ -1,4 +1,10 @@ import { UniqueAsset } from '@ghostfolio/common/interfaces'; +import { DateRange } from '@ghostfolio/common/types'; + +export interface IDateRangeOption { + label: string; + value: DateRange; +} export interface ISearchResultItem extends UniqueAsset { assetSubClassString: string;