Compare commits

..

33 Commits

Author SHA1 Message Date
0f9455cf02 Release 2.47.0 (#2951) 2024-02-03 09:44:29 +01:00
d4afa03505 Bugfix/fix rendering issue with date range selector of assistant (#2950)
* Improve click handling

* Improve locales

* Update changelog
2024-02-03 09:42:50 +01:00
c9237146e2 Feature/add investment value to chart (#2948)
* Add investment value to chart

* Update changelog
2024-02-03 09:23:19 +01:00
faad65b6f3 Update translations (#2940)
* Update translations

* Update changelog
2024-02-02 20:42:12 +01:00
e459c72100 Update OSS friends (#2942) 2024-02-01 20:54:25 +01:00
a8add30125 Feature/upgrade prettier to version 3.2.4 (#2928)
* Upgrade prettier to version 3.2.4

* Update changelog
2024-01-31 18:12:09 +01:00
b535aee91d Remove reference to Internet Identity (#2946) 2024-01-31 17:19:05 +01:00
4434d0315f Remove unused timeline calculation (#2947) 2024-01-30 20:56:41 +01:00
8b10695353 Feature/only show used tags in tag selector of assistant (#2943)
* Only show used tags in tag selector

* Update changelog
2024-01-29 19:53:47 +01:00
e82dcc8ace Feature/fix export in lazy-loaded activities table (#2939)
* Fix export in lazy-loaded activities table

* Update changelog

---------

Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
2024-01-29 19:37:09 +01:00
6dcb0d8583 Release 2.46.0 (#2938) 2024-01-28 10:00:07 +01:00
40b6777814 Add upgrade plan button (#2937) 2024-01-28 09:58:38 +01:00
25deba16df Feature/add reset filters button to assistant (#2936)
* Add reset filters button

* Update changelog
2024-01-28 09:47:28 +01:00
be93ca8968 Feature/migrate allocations page to work with filters of assistant (#2933)
* Migrate portfolio allocations to work with filters of assistant

* Update changelog
2024-01-28 09:20:32 +01:00
0436cc6487 Migrate ngx-skeleton-loader components to self-closing tags (#2935) 2024-01-28 08:51:02 +01:00
857708dc4d Migrate gf-* components to self-closing tags (#2934) 2024-01-28 08:50:43 +01:00
1ca4f885b0 Feature/migrate holdings page to work with filters of assistant (#2932)
* Migrate portfolio holdings to work with filters of assistant

* Update changelog
2024-01-27 19:28:13 +01:00
c9368c5cf2 Release 2.45.0 (#2931) 2024-01-27 10:54:35 +01:00
29423efea3 Update translations (#2930) 2024-01-27 10:53:19 +01:00
f3ee99fb2b Feature/extend assistant by account selector (#2929)
* Add account selector to assistant

* Update changelog
2024-01-27 10:48:46 +01:00
3df8810412 Feature/Add support to grant private access with permissions (#2870)
* Add support to grant private access with permissions

* Update changelog

---------

Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
2024-01-27 09:44:13 +01:00
b8ca88c6df Add auto-renewal (#2920) 2024-01-27 08:46:27 +01:00
2c068c412d Feature/migrate tag selector to form group in assistant (#2926)
* Introduce filter form group

* Update changelog
2024-01-27 08:45:55 +01:00
9fdbd22cb5 Bugfix/fix activities import for manual data source (#2923)
* Fix import

* Update changelog
2024-01-27 08:41:45 +01:00
8f5f4c5875 Feature/format name in eod historical data service (#2922)
* Format name

* Update changelog
2024-01-26 22:37:47 +01:00
50fb82a6e6 Extend personal finance tools (#2925) 2024-01-26 22:37:26 +01:00
2c10cd7edf Bugfix/remove holdings with incomplete data from top3 bottom3 performers (#2921)
* Remove holdings with incomplete data

* Update changelog
2024-01-26 08:35:23 +01:00
bbde86c66e Feature/improve language localization for de 20240124 (#2918)
* Update translations

* Update changelog
2024-01-25 21:11:07 +01:00
73c0843d51 Feature/add permissions to access model (#2833)
* Add permissions to Access model

* Update changelog
2024-01-24 19:23:58 +01:00
04fc2cd3e1 Release 2.44.0 (#2917) 2024-01-24 12:26:36 +01:00
b39c97ab9f Bugfix/improve validation for non numeric results in eod service (#2916)
* Improve validation of non-numeric numbers

* Update changelog
2024-01-24 12:24:38 +01:00
1dd5e9c787 Release 2.43.1 (#2914) 2024-01-23 20:46:37 +01:00
a9985b65b8 Adjust Dockerfile to enable healthcheck (#2913) 2024-01-23 20:44:57 +01:00
137 changed files with 6063 additions and 3314 deletions

View File

@ -5,7 +5,57 @@ 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/), 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 2.43.0 - 2024-01-23 ## 2.47.0 - 2024-02-02
### Changed
- Improved the tag selector to only show used tags in the assistant (experimental)
- Improved the language localization for German (`de`)
- Upgraded `prettier` from version `3.2.1` to `3.2.4`
### Fixed
- Fixed a rendering issue caused by the date range selector in the assistant (experimental)
- Fixed an issue with the currency conversion in the investment timeline
- Fixed the export in the lazy-loaded activities table on the portfolio activities page (experimental)
## 2.46.0 - 2024-01-28
### Added
- Added a button to reset the active filters in the assistant (experimental)
### Changed
- Migrated the portfolio allocations to work with the filters of the assistant (experimental)
- Migrated the portfolio holdings to work with the filters of the assistant (experimental)
## 2.45.0 - 2024-01-27
### Added
- Extended the assistant by an account selector (experimental)
- Added support to grant private access with permissions (experimental)
- Added `permissions` to the `Access` model
### Changed
- Migrated the tag selector to a form group in the assistant (experimental)
- Formatted the name in the _EOD Historical Data_ service
- Improved the language localization for German (`de`)
### Fixed
- Fixed the import for activities with `MANUAL` data source and type `FEE`, `INTEREST`, `ITEM` or `LIABILITY`
- Removed holdings with incomplete data from the _Top 3_ and _Bottom 3_ performers on the analysis page
## 2.44.0 - 2024-01-24
### Fixed
- Improved the validation for non-numeric results in the _EOD Historical Data_ service
## 2.43.1 - 2024-01-23
### Added ### Added

View File

@ -13,7 +13,6 @@ COPY ./.yarnrc .yarnrc
COPY ./prisma/schema.prisma prisma/schema.prisma COPY ./prisma/schema.prisma prisma/schema.prisma
RUN apt update && apt install -y \ RUN apt update && apt install -y \
curl \
g++ \ g++ \
git \ git \
make \ make \
@ -53,6 +52,7 @@ RUN yarn database:generate-typings
# Image to run, copy everything needed from builder # Image to run, copy everything needed from builder
FROM node:18-slim FROM node:18-slim
RUN apt update && apt install -y \ RUN apt update && apt install -y \
curl \
openssl \ openssl \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*

View File

@ -42,23 +42,27 @@ export class AccessController {
where: { userId: this.request.user.id } where: { userId: this.request.user.id }
}); });
return accessesWithGranteeUser.map((access) => { return accessesWithGranteeUser.map(
if (access.GranteeUser) { ({ alias, GranteeUser, id, permissions }) => {
if (GranteeUser) {
return {
alias,
id,
permissions,
grantee: GranteeUser?.id,
type: 'PRIVATE'
};
}
return { return {
alias: access.alias, alias,
grantee: access.GranteeUser?.id, id,
id: access.id, permissions,
type: 'RESTRICTED_VIEW' grantee: 'Public',
type: 'PUBLIC'
}; };
} }
);
return {
alias: access.alias,
grantee: 'Public',
id: access.id,
type: 'PUBLIC'
};
});
} }
@HasPermission(permissions.createAccess) @HasPermission(permissions.createAccess)
@ -83,6 +87,7 @@ export class AccessController {
GranteeUser: data.granteeUserId GranteeUser: data.granteeUserId
? { connect: { id: data.granteeUserId } } ? { connect: { id: data.granteeUserId } }
: undefined, : undefined,
permissions: data.permissions,
User: { connect: { id: this.request.user.id } } User: { connect: { id: this.request.user.id } }
}); });
} catch { } catch {

View File

@ -1,4 +1,5 @@
import { IsOptional, IsString, IsUUID } from 'class-validator'; import { AccessPermission } from '@prisma/client';
import { IsEnum, IsOptional, IsString, IsUUID } from 'class-validator';
export class CreateAccessDto { export class CreateAccessDto {
@IsOptional() @IsOptional()
@ -9,7 +10,7 @@ export class CreateAccessDto {
@IsUUID() @IsUUID()
granteeUserId?: string; granteeUserId?: string;
@IsEnum(AccessPermission, { each: true })
@IsOptional() @IsOptional()
@IsString() permissions?: AccessPermission[];
type?: 'PUBLIC';
} }

View File

@ -1,4 +1,5 @@
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { Export } from '@ghostfolio/common/interfaces'; import { Export } from '@ghostfolio/common/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types'; import type { RequestWithUser } from '@ghostfolio/common/types';
import { Controller, Get, Inject, Query, UseGuards } from '@nestjs/common'; import { Controller, Get, Inject, Query, UseGuards } from '@nestjs/common';
@ -10,6 +11,7 @@ import { ExportService } from './export.service';
@Controller('export') @Controller('export')
export class ExportController { export class ExportController {
public constructor( public constructor(
private readonly apiService: ApiService,
private readonly exportService: ExportService, private readonly exportService: ExportService,
@Inject(REQUEST) private readonly request: RequestWithUser @Inject(REQUEST) private readonly request: RequestWithUser
) {} ) {}
@ -17,10 +19,20 @@ export class ExportController {
@Get() @Get()
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async export( public async export(
@Query('activityIds') activityIds?: string[] @Query('accounts') filterByAccounts?: string,
@Query('activityIds') activityIds?: string[],
@Query('assetClasses') filterByAssetClasses?: string,
@Query('tags') filterByTags?: string
): Promise<Export> { ): Promise<Export> {
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
filterByTags
});
return this.exportService.export({ return this.exportService.export({
activityIds, activityIds,
filters,
userCurrency: this.request.user.Settings.settings.baseCurrency, userCurrency: this.request.user.Settings.settings.baseCurrency,
userId: this.request.user.id userId: this.request.user.id
}); });

View File

@ -1,6 +1,7 @@
import { AccountModule } from '@ghostfolio/api/app/account/account.module'; import { AccountModule } from '@ghostfolio/api/app/account/account.module';
import { OrderModule } from '@ghostfolio/api/app/order/order.module'; import { OrderModule } from '@ghostfolio/api/app/order/order.module';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module'; import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
@ -12,6 +13,7 @@ import { ExportService } from './export.service';
@Module({ @Module({
imports: [ imports: [
AccountModule, AccountModule,
ApiModule,
ConfigurationModule, ConfigurationModule,
DataGatheringModule, DataGatheringModule,
DataProviderModule, DataProviderModule,

View File

@ -1,7 +1,7 @@
import { AccountService } from '@ghostfolio/api/app/account/account.service'; import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { OrderService } from '@ghostfolio/api/app/order/order.service'; import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { environment } from '@ghostfolio/api/environments/environment'; import { environment } from '@ghostfolio/api/environments/environment';
import { Export } from '@ghostfolio/common/interfaces'; import { Filter, Export } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
@Injectable() @Injectable()
@ -13,10 +13,12 @@ export class ExportService {
public async export({ public async export({
activityIds, activityIds,
filters,
userCurrency, userCurrency,
userId userId
}: { }: {
activityIds?: string[]; activityIds?: string[];
filters?: Filter[];
userCurrency: string; userCurrency: string;
userId: string; userId: string;
}): Promise<Export> { }): Promise<Export> {
@ -42,6 +44,7 @@ export class ExportService {
); );
let { activities } = await this.orderService.getOrders({ let { activities } = await this.orderService.getOrders({
filters,
userCurrency, userCurrency,
userId, userId,
includeDrafts: true, includeDrafts: true,

View File

@ -575,7 +575,7 @@ export class ImportService {
for (const [ for (const [
index, index,
{ currency, dataSource, symbol } { currency, dataSource, symbol, type }
] of uniqueActivitiesDto.entries()) { ] of uniqueActivitiesDto.entries()) {
if (!this.configurationService.get('DATA_SOURCES').includes(dataSource)) { if (!this.configurationService.get('DATA_SOURCES').includes(dataSource)) {
throw new Error( throw new Error(
@ -583,28 +583,33 @@ export class ImportService {
); );
} }
const assetProfile = ( const assetProfile = {
await this.dataProviderService.getAssetProfiles([ currency,
{ dataSource, symbol } ...(
]) await this.dataProviderService.getAssetProfiles([
)?.[symbol]; { dataSource, symbol }
])
)?.[symbol]
};
if (!assetProfile?.name) { if (type === 'BUY' || type === 'DIVIDEND' || type === 'SELL') {
throw new Error( if (!assetProfile?.name) {
`activities.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")` throw new Error(
); `activities.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")`
} );
}
if ( if (
assetProfile.currency !== currency && assetProfile.currency !== currency &&
!this.exchangeRateDataService.hasCurrencyPair( !this.exchangeRateDataService.hasCurrencyPair(
currency, currency,
assetProfile.currency assetProfile.currency
) )
) { ) {
throw new Error( throw new Error(
`activities.${index}.currency ("${currency}") does not match with "${assetProfile.currency}" and no exchange rate is available from "${currency}" to "${assetProfile.currency}"` `activities.${index}.currency ("${currency}") does not match with "${assetProfile.currency}" and no exchange rate is available from "${currency}" to "${assetProfile.currency}"`
); );
}
} }
assetProfiles[getAssetProfileIdentifier({ dataSource, symbol })] = assetProfiles[getAssetProfileIdentifier({ dataSource, symbol })] =

View File

@ -1,8 +0,0 @@
import { TimelinePeriod } from '@ghostfolio/api/app/portfolio/interfaces/timeline-period.interface';
import Big from 'big.js';
export interface TimelineInfoInterface {
maxNetPerformance: Big;
minNetPerformance: Big;
timelinePeriods: TimelinePeriod[];
}

View File

@ -1,9 +0,0 @@
import Big from 'big.js';
export interface TimelinePeriod {
date: string;
grossPerformance: Big;
investment: Big;
netPerformance: Big;
value: Big;
}

View File

@ -1,6 +0,0 @@
export type Accuracy = 'day' | 'month' | 'year';
export interface TimelineSpecification {
accuracy: Accuracy;
start: string;
}

View File

@ -68,14 +68,20 @@ describe('PortfolioCalculator', () => {
.spyOn(Date, 'now') .spyOn(Date, 'now')
.mockImplementation(() => parseDate('2021-12-18').getTime()); .mockImplementation(() => parseDate('2021-12-18').getTime());
const chartData = await portfolioCalculator.getChartData({
start: parseDate('2021-11-22')
});
const currentPositions = await portfolioCalculator.getCurrentPositions( const currentPositions = await portfolioCalculator.getCurrentPositions(
parseDate('2021-11-22') parseDate('2021-11-22')
); );
const investments = portfolioCalculator.getInvestments(); const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
portfolioCalculator.getInvestmentsByGroup('month'); data: chartData,
groupBy: 'month'
});
spy.mockRestore(); spy.mockRestore();
@ -135,7 +141,8 @@ describe('PortfolioCalculator', () => {
]); ]);
expect(investmentsByMonth).toEqual([ expect(investmentsByMonth).toEqual([
{ date: '2021-11-01', investment: new Big('12.6') } { date: '2021-11-01', investment: 0 },
{ date: '2021-12-01', investment: 0 }
]); ]);
}); });
}); });

View File

@ -57,14 +57,20 @@ describe('PortfolioCalculator', () => {
.spyOn(Date, 'now') .spyOn(Date, 'now')
.mockImplementation(() => parseDate('2021-12-18').getTime()); .mockImplementation(() => parseDate('2021-12-18').getTime());
const chartData = await portfolioCalculator.getChartData({
start: parseDate('2021-11-30')
});
const currentPositions = await portfolioCalculator.getCurrentPositions( const currentPositions = await portfolioCalculator.getCurrentPositions(
parseDate('2021-11-30') parseDate('2021-11-30')
); );
const investments = portfolioCalculator.getInvestments(); const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
portfolioCalculator.getInvestmentsByGroup('month'); data: chartData,
groupBy: 'month'
});
spy.mockRestore(); spy.mockRestore();
@ -123,7 +129,8 @@ describe('PortfolioCalculator', () => {
]); ]);
expect(investmentsByMonth).toEqual([ expect(investmentsByMonth).toEqual([
{ date: '2021-11-01', investment: new Big('273.2') } { date: '2021-11-01', investment: 273.2 },
{ date: '2021-12-01', investment: 0 }
]); ]);
}); });
}); });

View File

@ -81,14 +81,20 @@ describe('PortfolioCalculator', () => {
.spyOn(Date, 'now') .spyOn(Date, 'now')
.mockImplementation(() => parseDate('2018-01-01').getTime()); .mockImplementation(() => parseDate('2018-01-01').getTime());
const chartData = await portfolioCalculator.getChartData({
start: parseDate('2015-01-01')
});
const currentPositions = await portfolioCalculator.getCurrentPositions( const currentPositions = await portfolioCalculator.getCurrentPositions(
parseDate('2015-01-01') parseDate('2015-01-01')
); );
const investments = portfolioCalculator.getInvestments(); const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
portfolioCalculator.getInvestmentsByGroup('month'); data: chartData,
groupBy: 'month'
});
spy.mockRestore(); spy.mockRestore();
@ -155,42 +161,43 @@ describe('PortfolioCalculator', () => {
]); ]);
expect(investmentsByMonth).toEqual([ expect(investmentsByMonth).toEqual([
{ date: '2015-01-01', investment: new Big('640.86') }, { date: '2015-01-01', investment: 637.0853345999999 },
{ date: '2015-02-01', investment: new Big('0') }, { date: '2015-02-01', investment: 0 },
{ date: '2015-03-01', investment: new Big('0') }, { date: '2015-03-01', investment: 0 },
{ date: '2015-04-01', investment: new Big('0') }, { date: '2015-04-01', investment: 0 },
{ date: '2015-05-01', investment: new Big('0') }, { date: '2015-05-01', investment: 0 },
{ date: '2015-06-01', investment: new Big('0') }, { date: '2015-06-01', investment: 0 },
{ date: '2015-07-01', investment: new Big('0') }, { date: '2015-07-01', investment: 0 },
{ date: '2015-08-01', investment: new Big('0') }, { date: '2015-08-01', investment: 0 },
{ date: '2015-09-01', investment: new Big('0') }, { date: '2015-09-01', investment: 0 },
{ date: '2015-10-01', investment: new Big('0') }, { date: '2015-10-01', investment: 0 },
{ date: '2015-11-01', investment: new Big('0') }, { date: '2015-11-01', investment: 0 },
{ date: '2015-12-01', investment: new Big('0') }, { date: '2015-12-01', investment: 0 },
{ date: '2016-01-01', investment: new Big('0') }, { date: '2016-01-01', investment: 0 },
{ date: '2016-02-01', investment: new Big('0') }, { date: '2016-02-01', investment: 0 },
{ date: '2016-03-01', investment: new Big('0') }, { date: '2016-03-01', investment: 0 },
{ date: '2016-04-01', investment: new Big('0') }, { date: '2016-04-01', investment: 0 },
{ date: '2016-05-01', investment: new Big('0') }, { date: '2016-05-01', investment: 0 },
{ date: '2016-06-01', investment: new Big('0') }, { date: '2016-06-01', investment: 0 },
{ date: '2016-07-01', investment: new Big('0') }, { date: '2016-07-01', investment: 0 },
{ date: '2016-08-01', investment: new Big('0') }, { date: '2016-08-01', investment: 0 },
{ date: '2016-09-01', investment: new Big('0') }, { date: '2016-09-01', investment: 0 },
{ date: '2016-10-01', investment: new Big('0') }, { date: '2016-10-01', investment: 0 },
{ date: '2016-11-01', investment: new Big('0') }, { date: '2016-11-01', investment: 0 },
{ date: '2016-12-01', investment: new Big('0') }, { date: '2016-12-01', investment: 0 },
{ date: '2017-01-01', investment: new Big('0') }, { date: '2017-01-01', investment: 0 },
{ date: '2017-02-01', investment: new Big('0') }, { date: '2017-02-01', investment: 0 },
{ date: '2017-03-01', investment: new Big('0') }, { date: '2017-03-01', investment: 0 },
{ date: '2017-04-01', investment: new Big('0') }, { date: '2017-04-01', investment: 0 },
{ date: '2017-05-01', investment: new Big('0') }, { date: '2017-05-01', investment: 0 },
{ date: '2017-06-01', investment: new Big('0') }, { date: '2017-06-01', investment: 0 },
{ date: '2017-07-01', investment: new Big('0') }, { date: '2017-07-01', investment: 0 },
{ date: '2017-08-01', investment: new Big('0') }, { date: '2017-08-01', investment: 0 },
{ date: '2017-09-01', investment: new Big('0') }, { date: '2017-09-01', investment: 0 },
{ date: '2017-10-01', investment: new Big('0') }, { date: '2017-10-01', investment: 0 },
{ date: '2017-11-01', investment: new Big('0') }, { date: '2017-11-01', investment: 0 },
{ date: '2017-12-01', investment: new Big('-14156.4') } { date: '2017-12-01', investment: -318.54266729999995 },
{ date: '2018-01-01', investment: 0 }
]); ]);
}); });
}); });

View File

@ -70,14 +70,20 @@ describe('PortfolioCalculator', () => {
.spyOn(Date, 'now') .spyOn(Date, 'now')
.mockImplementation(() => parseDate('2023-07-10').getTime()); .mockImplementation(() => parseDate('2023-07-10').getTime());
const chartData = await portfolioCalculator.getChartData({
start: parseDate('2023-01-03')
});
const currentPositions = await portfolioCalculator.getCurrentPositions( const currentPositions = await portfolioCalculator.getCurrentPositions(
parseDate('2023-01-03') parseDate('2023-01-03')
); );
const investments = portfolioCalculator.getInvestments(); const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
portfolioCalculator.getInvestmentsByGroup('month'); data: chartData,
groupBy: 'month'
});
spy.mockRestore(); spy.mockRestore();
@ -137,7 +143,31 @@ describe('PortfolioCalculator', () => {
]); ]);
expect(investmentsByMonth).toEqual([ expect(investmentsByMonth).toEqual([
{ date: '2023-01-01', investment: new Big('89.12') } { date: '2023-01-01', investment: 82.329056 },
{
date: '2023-02-01',
investment: 0
},
{
date: '2023-03-01',
investment: 0
},
{
date: '2023-04-01',
investment: 0
},
{
date: '2023-05-01',
investment: 0
},
{
date: '2023-06-01',
investment: 0
},
{
date: '2023-07-01',
investment: 0
}
]); ]);
}); });
}); });

View File

@ -45,14 +45,20 @@ describe('PortfolioCalculator', () => {
.spyOn(Date, 'now') .spyOn(Date, 'now')
.mockImplementation(() => parseDate('2021-12-18').getTime()); .mockImplementation(() => parseDate('2021-12-18').getTime());
const chartData = await portfolioCalculator.getChartData({
start: new Date()
});
const currentPositions = await portfolioCalculator.getCurrentPositions( const currentPositions = await portfolioCalculator.getCurrentPositions(
new Date() new Date()
); );
const investments = portfolioCalculator.getInvestments(); const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
portfolioCalculator.getInvestmentsByGroup('month'); data: chartData,
groupBy: 'month'
});
spy.mockRestore(); spy.mockRestore();

View File

@ -68,14 +68,20 @@ describe('PortfolioCalculator', () => {
.spyOn(Date, 'now') .spyOn(Date, 'now')
.mockImplementation(() => parseDate('2022-04-11').getTime()); .mockImplementation(() => parseDate('2022-04-11').getTime());
const chartData = await portfolioCalculator.getChartData({
start: parseDate('2022-03-07')
});
const currentPositions = await portfolioCalculator.getCurrentPositions( const currentPositions = await portfolioCalculator.getCurrentPositions(
parseDate('2022-03-07') parseDate('2022-03-07')
); );
const investments = portfolioCalculator.getInvestments(); const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
portfolioCalculator.getInvestmentsByGroup('month'); data: chartData,
groupBy: 'month'
});
spy.mockRestore(); spy.mockRestore();
@ -137,8 +143,8 @@ describe('PortfolioCalculator', () => {
]); ]);
expect(investmentsByMonth).toEqual([ expect(investmentsByMonth).toEqual([
{ date: '2022-03-01', investment: new Big('151.6') }, { date: '2022-03-01', investment: 151.6 },
{ date: '2022-04-01', investment: new Big('-85.73') } { date: '2022-04-01', investment: -75.8 }
]); ]);
}); });
}); });

View File

@ -68,9 +68,9 @@ describe('PortfolioCalculator', () => {
.spyOn(Date, 'now') .spyOn(Date, 'now')
.mockImplementation(() => parseDate('2022-04-11').getTime()); .mockImplementation(() => parseDate('2022-04-11').getTime());
const chartData = await portfolioCalculator.getChartData( const chartData = await portfolioCalculator.getChartData({
parseDate('2022-03-07') start: parseDate('2022-03-07')
); });
const currentPositions = await portfolioCalculator.getCurrentPositions( const currentPositions = await portfolioCalculator.getCurrentPositions(
parseDate('2022-03-07') parseDate('2022-03-07')
@ -78,13 +78,16 @@ describe('PortfolioCalculator', () => {
const investments = portfolioCalculator.getInvestments(); const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
portfolioCalculator.getInvestmentsByGroup('month'); data: chartData,
groupBy: 'month'
});
spy.mockRestore(); spy.mockRestore();
expect(chartData[0]).toEqual({ expect(chartData[0]).toEqual({
date: '2022-03-07', date: '2022-03-07',
investmentValueWithCurrencyEffect: 151.6,
netPerformance: 0, netPerformance: 0,
netPerformanceInPercentage: 0, netPerformanceInPercentage: 0,
netPerformanceInPercentageWithCurrencyEffect: 0, netPerformanceInPercentageWithCurrencyEffect: 0,
@ -97,6 +100,7 @@ describe('PortfolioCalculator', () => {
expect(chartData[chartData.length - 1]).toEqual({ expect(chartData[chartData.length - 1]).toEqual({
date: '2022-04-11', date: '2022-04-11',
investmentValueWithCurrencyEffect: 0,
netPerformance: 19.86, netPerformance: 19.86,
netPerformanceInPercentage: 13.100263852242744, netPerformanceInPercentage: 13.100263852242744,
netPerformanceInPercentageWithCurrencyEffect: 13.100263852242744, netPerformanceInPercentageWithCurrencyEffect: 13.100263852242744,
@ -163,8 +167,8 @@ describe('PortfolioCalculator', () => {
]); ]);
expect(investmentsByMonth).toEqual([ expect(investmentsByMonth).toEqual([
{ date: '2022-03-01', investment: new Big('151.6') }, { date: '2022-03-01', investment: 151.6 },
{ date: '2022-04-01', investment: new Big('-171.46') } { date: '2022-04-01', investment: -151.6 }
]); ]);
}); });
}); });

View File

@ -1,9 +1,10 @@
import { TimelineInfoInterface } from '@ghostfolio/api/app/portfolio/interfaces/timeline-info.interface';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
import { import {
DataProviderInfo, DataProviderInfo,
HistoricalDataItem,
InvestmentItem,
ResponseError, ResponseError,
SymbolMetrics, SymbolMetrics,
TimelinePosition TimelinePosition
@ -15,41 +16,19 @@ import Big from 'big.js';
import { import {
addDays, addDays,
addMilliseconds, addMilliseconds,
addMonths,
addYears,
differenceInDays, differenceInDays,
endOfDay, endOfDay,
format, format,
isAfter,
isBefore, isBefore,
isSameDay, isSameDay,
isSameMonth,
isSameYear,
max,
min,
set,
subDays subDays
} from 'date-fns'; } from 'date-fns';
import { import { cloneDeep, first, isNumber, last, sortBy, uniq } from 'lodash';
cloneDeep,
first,
flatten,
isNumber,
last,
sortBy,
uniq
} from 'lodash';
import { CurrentRateService } from './current-rate.service'; import { CurrentRateService } from './current-rate.service';
import { CurrentPositions } from './interfaces/current-positions.interface'; import { CurrentPositions } from './interfaces/current-positions.interface';
import { GetValueObject } from './interfaces/get-value-object.interface';
import { PortfolioOrderItem } from './interfaces/portfolio-calculator.interface'; import { PortfolioOrderItem } from './interfaces/portfolio-calculator.interface';
import { PortfolioOrder } from './interfaces/portfolio-order.interface'; import { PortfolioOrder } from './interfaces/portfolio-order.interface';
import { TimelinePeriod } from './interfaces/timeline-period.interface';
import {
Accuracy,
TimelineSpecification
} from './interfaces/timeline-specification.interface';
import { TransactionPointSymbol } from './interfaces/transaction-point-symbol.interface'; import { TransactionPointSymbol } from './interfaces/transaction-point-symbol.interface';
import { TransactionPoint } from './interfaces/transaction-point.interface'; import { TransactionPoint } from './interfaces/transaction-point.interface';
@ -193,7 +172,15 @@ export class PortfolioCalculator {
this.transactionPoints = transactionPoints; this.transactionPoints = transactionPoints;
} }
public async getChartData(start: Date, end = new Date(Date.now()), step = 1) { public async getChartData({
end = new Date(Date.now()),
start,
step = 1
}: {
end?: Date;
start: Date;
step?: number;
}): Promise<HistoricalDataItem[]> {
const symbols: { [symbol: string]: boolean } = {}; const symbols: { [symbol: string]: boolean } = {};
const transactionPointsBeforeEndDate = const transactionPointsBeforeEndDate =
@ -217,13 +204,15 @@ export class PortfolioCalculator {
dates.push(resetHours(end)); dates.push(resetHours(end));
} }
for (const item of transactionPointsBeforeEndDate[firstIndex - 1].items) { if (transactionPointsBeforeEndDate.length > 0) {
dataGatheringItems.push({ for (const item of transactionPointsBeforeEndDate[firstIndex - 1].items) {
dataSource: item.dataSource, dataGatheringItems.push({
symbol: item.symbol dataSource: item.dataSource,
}); symbol: item.symbol
currencies[item.symbol] = item.currency; });
symbols[item.symbol] = true; currencies[item.symbol] = item.currency;
symbols[item.symbol] = true;
}
} }
const { dataProviderInfos, values: marketSymbols } = const { dataProviderInfos, values: marketSymbols } =
@ -262,6 +251,7 @@ export class PortfolioCalculator {
const accumulatedValuesByDate: { const accumulatedValuesByDate: {
[date: string]: { [date: string]: {
investmentValueWithCurrencyEffect: Big;
totalCurrentValue: Big; totalCurrentValue: Big;
totalCurrentValueWithCurrencyEffect: Big; totalCurrentValueWithCurrencyEffect: Big;
totalInvestmentValue: Big; totalInvestmentValue: Big;
@ -277,7 +267,8 @@ export class PortfolioCalculator {
[symbol: string]: { [symbol: string]: {
currentValues: { [date: string]: Big }; currentValues: { [date: string]: Big };
currentValuesWithCurrencyEffect: { [date: string]: Big }; currentValuesWithCurrencyEffect: { [date: string]: Big };
investmentValues: { [date: string]: Big }; investmentValuesAccumulated: { [date: string]: Big };
investmentValuesAccumulatedWithCurrencyEffect: { [date: string]: Big };
investmentValuesWithCurrencyEffect: { [date: string]: Big }; investmentValuesWithCurrencyEffect: { [date: string]: Big };
netPerformanceValues: { [date: string]: Big }; netPerformanceValues: { [date: string]: Big };
netPerformanceValuesWithCurrencyEffect: { [date: string]: Big }; netPerformanceValuesWithCurrencyEffect: { [date: string]: Big };
@ -290,7 +281,8 @@ export class PortfolioCalculator {
const { const {
currentValues, currentValues,
currentValuesWithCurrencyEffect, currentValuesWithCurrencyEffect,
investmentValues, investmentValuesAccumulated,
investmentValuesAccumulatedWithCurrencyEffect,
investmentValuesWithCurrencyEffect, investmentValuesWithCurrencyEffect,
netPerformanceValues, netPerformanceValues,
netPerformanceValuesWithCurrencyEffect, netPerformanceValuesWithCurrencyEffect,
@ -310,7 +302,8 @@ export class PortfolioCalculator {
valuesBySymbol[symbol] = { valuesBySymbol[symbol] = {
currentValues, currentValues,
currentValuesWithCurrencyEffect, currentValuesWithCurrencyEffect,
investmentValues, investmentValuesAccumulated,
investmentValuesAccumulatedWithCurrencyEffect,
investmentValuesWithCurrencyEffect, investmentValuesWithCurrencyEffect,
netPerformanceValues, netPerformanceValues,
netPerformanceValuesWithCurrencyEffect, netPerformanceValuesWithCurrencyEffect,
@ -332,8 +325,13 @@ export class PortfolioCalculator {
symbolValues.currentValuesWithCurrencyEffect?.[dateString] ?? symbolValues.currentValuesWithCurrencyEffect?.[dateString] ??
new Big(0); new Big(0);
const investmentValue = const investmentValueAccumulated =
symbolValues.investmentValues?.[dateString] ?? new Big(0); symbolValues.investmentValuesAccumulated?.[dateString] ?? new Big(0);
const investmentValueAccumulatedWithCurrencyEffect =
symbolValues.investmentValuesAccumulatedWithCurrencyEffect?.[
dateString
] ?? new Big(0);
const investmentValueWithCurrencyEffect = const investmentValueWithCurrencyEffect =
symbolValues.investmentValuesWithCurrencyEffect?.[dateString] ?? symbolValues.investmentValuesWithCurrencyEffect?.[dateString] ??
@ -355,6 +353,10 @@ export class PortfolioCalculator {
] ?? new Big(0); ] ?? new Big(0);
accumulatedValuesByDate[dateString] = { accumulatedValuesByDate[dateString] = {
investmentValueWithCurrencyEffect: (
accumulatedValuesByDate[dateString]
?.investmentValueWithCurrencyEffect ?? new Big(0)
).add(investmentValueWithCurrencyEffect),
totalCurrentValue: ( totalCurrentValue: (
accumulatedValuesByDate[dateString]?.totalCurrentValue ?? new Big(0) accumulatedValuesByDate[dateString]?.totalCurrentValue ?? new Big(0)
).add(currentValue), ).add(currentValue),
@ -365,11 +367,11 @@ export class PortfolioCalculator {
totalInvestmentValue: ( totalInvestmentValue: (
accumulatedValuesByDate[dateString]?.totalInvestmentValue ?? accumulatedValuesByDate[dateString]?.totalInvestmentValue ??
new Big(0) new Big(0)
).add(investmentValue), ).add(investmentValueAccumulated),
totalInvestmentValueWithCurrencyEffect: ( totalInvestmentValueWithCurrencyEffect: (
accumulatedValuesByDate[dateString] accumulatedValuesByDate[dateString]
?.totalInvestmentValueWithCurrencyEffect ?? new Big(0) ?.totalInvestmentValueWithCurrencyEffect ?? new Big(0)
).add(investmentValueWithCurrencyEffect), ).add(investmentValueAccumulatedWithCurrencyEffect),
totalNetPerformanceValue: ( totalNetPerformanceValue: (
accumulatedValuesByDate[dateString]?.totalNetPerformanceValue ?? accumulatedValuesByDate[dateString]?.totalNetPerformanceValue ??
new Big(0) new Big(0)
@ -392,6 +394,7 @@ export class PortfolioCalculator {
return Object.entries(accumulatedValuesByDate).map(([date, values]) => { return Object.entries(accumulatedValuesByDate).map(([date, values]) => {
const { const {
investmentValueWithCurrencyEffect,
totalCurrentValue, totalCurrentValue,
totalCurrentValueWithCurrencyEffect, totalCurrentValueWithCurrencyEffect,
totalInvestmentValue, totalInvestmentValue,
@ -421,6 +424,8 @@ export class PortfolioCalculator {
date, date,
netPerformanceInPercentage, netPerformanceInPercentage,
netPerformanceInPercentageWithCurrencyEffect, netPerformanceInPercentageWithCurrencyEffect,
investmentValueWithCurrencyEffect:
investmentValueWithCurrencyEffect.toNumber(),
netPerformance: totalNetPerformanceValue.toNumber(), netPerformance: totalNetPerformanceValue.toNumber(),
netPerformanceWithCurrencyEffect: netPerformanceWithCurrencyEffect:
totalNetPerformanceValueWithCurrencyEffect.toNumber(), totalNetPerformanceValueWithCurrencyEffect.toNumber(),
@ -685,196 +690,27 @@ export class PortfolioCalculator {
}); });
} }
public getInvestmentsByGroup( public getInvestmentsByGroup({
groupBy: GroupBy data,
): { date: string; investment: Big }[] { groupBy
if (this.orders.length === 0) { }: {
return []; data: HistoricalDataItem[];
} groupBy: GroupBy;
}): InvestmentItem[] {
const groupedData: { [dateGroup: string]: Big } = {};
const investments: { date: string; investment: Big }[] = []; for (const { date, investmentValueWithCurrencyEffect } of data) {
let currentDate: Date; const dateGroup =
let investmentByGroup = new Big(0); groupBy === 'month' ? date.substring(0, 7) : date.substring(0, 4);
groupedData[dateGroup] = (groupedData[dateGroup] ?? new Big(0)).plus(
for (const [index, order] of this.orders.entries()) { investmentValueWithCurrencyEffect
if (
isSameYear(parseDate(order.date), currentDate) &&
(groupBy === 'year' || isSameMonth(parseDate(order.date), currentDate))
) {
// Same group: Add up investments
investmentByGroup = investmentByGroup.plus(
order.quantity.mul(order.unitPrice).mul(this.getFactor(order.type))
);
} else {
// New group: Store previous group and reset
if (currentDate) {
investments.push({
date: format(
set(currentDate, {
date: 1,
month: groupBy === 'year' ? 0 : currentDate.getMonth()
}),
DATE_FORMAT
),
investment: investmentByGroup
});
}
currentDate = parseDate(order.date);
investmentByGroup = order.quantity
.mul(order.unitPrice)
.mul(this.getFactor(order.type));
}
if (index === this.orders.length - 1) {
// Store current group (latest order)
investments.push({
date: format(
set(currentDate, {
date: 1,
month: groupBy === 'year' ? 0 : currentDate.getMonth()
}),
DATE_FORMAT
),
investment: investmentByGroup
});
}
}
// Fill in the missing dates with investment = 0
const startDate = parseDate(first(this.orders).date);
const endDate = parseDate(last(this.orders).date);
const allDates: string[] = [];
currentDate = startDate;
while (currentDate <= endDate) {
allDates.push(
format(
set(currentDate, {
date: 1,
month: groupBy === 'year' ? 0 : currentDate.getMonth()
}),
DATE_FORMAT
)
); );
currentDate.setMonth(currentDate.getMonth() + 1);
} }
for (const date of allDates) { return Object.keys(groupedData).map((dateGroup) => ({
const existingInvestment = investments.find((investment) => { date: groupBy === 'month' ? `${dateGroup}-01` : `${dateGroup}-01-01`,
return investment.date === date; investment: groupedData[dateGroup].toNumber()
}); }));
if (!existingInvestment) {
investments.push({ date, investment: new Big(0) });
}
}
return sortBy(investments, ({ date }) => {
return date;
});
}
public async calculateTimeline(
timelineSpecification: TimelineSpecification[],
endDate: string
): Promise<TimelineInfoInterface> {
if (timelineSpecification.length === 0) {
return {
maxNetPerformance: new Big(0),
minNetPerformance: new Big(0),
timelinePeriods: []
};
}
const startDate = timelineSpecification[0].start;
const start = parseDate(startDate);
const end = parseDate(endDate);
const timelinePeriodPromises: Promise<TimelineInfoInterface>[] = [];
let i = 0;
let j = -1;
for (
let currentDate = start;
!isAfter(currentDate, end);
currentDate = this.addToDate(
currentDate,
timelineSpecification[i].accuracy
)
) {
if (this.isNextItemActive(timelineSpecification, currentDate, i)) {
i++;
}
while (
j + 1 < this.transactionPoints.length &&
!isAfter(parseDate(this.transactionPoints[j + 1].date), currentDate)
) {
j++;
}
let periodEndDate = currentDate;
if (timelineSpecification[i].accuracy === 'day') {
let nextEndDate = end;
if (j + 1 < this.transactionPoints.length) {
nextEndDate = parseDate(this.transactionPoints[j + 1].date);
}
periodEndDate = min([
addMonths(currentDate, 3),
max([currentDate, nextEndDate])
]);
}
const timePeriodForDates = this.getTimePeriodForDate(
j,
currentDate,
endOfDay(periodEndDate)
);
currentDate = periodEndDate;
if (timePeriodForDates != null) {
timelinePeriodPromises.push(timePeriodForDates);
}
}
let minNetPerformance = new Big(0);
let maxNetPerformance = new Big(0);
const timelineInfoInterfaces: TimelineInfoInterface[] = await Promise.all(
timelinePeriodPromises
);
try {
minNetPerformance = timelineInfoInterfaces
.map((timelineInfo) => timelineInfo.minNetPerformance)
.filter((performance) => performance !== null)
.reduce((minPerformance, current) => {
if (minPerformance.lt(current)) {
return minPerformance;
} else {
return current;
}
});
maxNetPerformance = timelineInfoInterfaces
.map((timelineInfo) => timelineInfo.maxNetPerformance)
.filter((performance) => performance !== null)
.reduce((maxPerformance, current) => {
if (maxPerformance.gt(current)) {
return maxPerformance;
} else {
return current;
}
});
} catch {}
const timelinePeriods = timelineInfoInterfaces.map(
(timelineInfo) => timelineInfo.timelinePeriods
);
return {
maxNetPerformance,
minNetPerformance,
timelinePeriods: flatten(timelinePeriods)
};
} }
private calculateOverallPerformance(positions: TimelinePosition[]) { private calculateOverallPerformance(positions: TimelinePosition[]) {
@ -983,123 +819,6 @@ export class PortfolioCalculator {
}; };
} }
private async getTimePeriodForDate(
j: number,
startDate: Date,
endDate: Date
): Promise<TimelineInfoInterface> {
let investment: Big = new Big(0);
let fees: Big = new Big(0);
const marketSymbolMap: {
[date: string]: { [symbol: string]: Big };
} = {};
if (j >= 0) {
const currencies: { [name: string]: string } = {};
const dataGatheringItems: IDataGatheringItem[] = [];
for (const item of this.transactionPoints[j].items) {
currencies[item.symbol] = item.currency;
dataGatheringItems.push({
dataSource: item.dataSource,
symbol: item.symbol
});
investment = investment.plus(item.investment);
fees = fees.plus(item.fee);
}
let marketSymbols: GetValueObject[] = [];
if (dataGatheringItems.length > 0) {
try {
const { values } = await this.currentRateService.getValues({
dataGatheringItems,
dateQuery: {
gte: startDate,
lt: endOfDay(endDate)
}
});
marketSymbols = values;
} catch (error) {
Logger.error(
`Failed to fetch info for date ${startDate} with exception`,
error,
'PortfolioCalculator'
);
return null;
}
}
for (const marketSymbol of marketSymbols) {
const date = format(marketSymbol.date, DATE_FORMAT);
if (!marketSymbolMap[date]) {
marketSymbolMap[date] = {};
}
if (marketSymbol.marketPrice) {
marketSymbolMap[date][marketSymbol.symbol] = new Big(
marketSymbol.marketPrice
);
}
}
}
const results: TimelinePeriod[] = [];
let maxNetPerformance: Big = null;
let minNetPerformance: Big = null;
for (
let currentDate = startDate;
isBefore(currentDate, endDate);
currentDate = addDays(currentDate, 1)
) {
let value = new Big(0);
const currentDateAsString = format(currentDate, DATE_FORMAT);
let invalid = false;
if (j >= 0) {
for (const item of this.transactionPoints[j].items) {
if (
!marketSymbolMap[currentDateAsString]?.hasOwnProperty(item.symbol)
) {
invalid = true;
break;
}
value = value.plus(
item.quantity.mul(marketSymbolMap[currentDateAsString][item.symbol])
);
}
}
if (!invalid) {
const grossPerformance = value.minus(investment);
const netPerformance = grossPerformance.minus(fees);
if (
minNetPerformance === null ||
minNetPerformance.gt(netPerformance)
) {
minNetPerformance = netPerformance;
}
if (
maxNetPerformance === null ||
maxNetPerformance.lt(netPerformance)
) {
maxNetPerformance = netPerformance;
}
const result = {
grossPerformance,
investment,
netPerformance,
value,
date: currentDateAsString
};
results.push(result);
}
}
return {
maxNetPerformance,
minNetPerformance,
timelinePeriods: results
};
}
private getFactor(type: TypeOfOrder) { private getFactor(type: TypeOfOrder) {
let factor: number; let factor: number;
@ -1118,17 +837,6 @@ export class PortfolioCalculator {
return factor; return factor;
} }
private addToDate(date: Date, accuracy: Accuracy): Date {
switch (accuracy) {
case 'day':
return addDays(date, 1);
case 'month':
return addMonths(date, 1);
case 'year':
return addYears(date, 1);
}
}
private getSymbolMetrics({ private getSymbolMetrics({
end, end,
exchangeRates, exchangeRates,
@ -1165,7 +873,10 @@ export class PortfolioCalculator {
let initialValueWithCurrencyEffect: Big; let initialValueWithCurrencyEffect: Big;
let investmentAtStartDate: Big; let investmentAtStartDate: Big;
let investmentAtStartDateWithCurrencyEffect: Big; let investmentAtStartDateWithCurrencyEffect: Big;
const investmentValues: { [date: string]: Big } = {}; const investmentValuesAccumulated: { [date: string]: Big } = {};
const investmentValuesAccumulatedWithCurrencyEffect: {
[date: string]: Big;
} = {};
const investmentValuesWithCurrencyEffect: { [date: string]: Big } = {}; const investmentValuesWithCurrencyEffect: { [date: string]: Big } = {};
let lastAveragePrice = new Big(0); let lastAveragePrice = new Big(0);
let lastAveragePriceWithCurrencyEffect = new Big(0); let lastAveragePriceWithCurrencyEffect = new Big(0);
@ -1207,7 +918,8 @@ export class PortfolioCalculator {
hasErrors: false, hasErrors: false,
initialValue: new Big(0), initialValue: new Big(0),
initialValueWithCurrencyEffect: new Big(0), initialValueWithCurrencyEffect: new Big(0),
investmentValues: {}, investmentValuesAccumulated: {},
investmentValuesAccumulatedWithCurrencyEffect: {},
investmentValuesWithCurrencyEffect: {}, investmentValuesWithCurrencyEffect: {},
netPerformance: new Big(0), netPerformance: new Big(0),
netPerformancePercentage: new Big(0), netPerformancePercentage: new Big(0),
@ -1246,7 +958,8 @@ export class PortfolioCalculator {
hasErrors: true, hasErrors: true,
initialValue: new Big(0), initialValue: new Big(0),
initialValueWithCurrencyEffect: new Big(0), initialValueWithCurrencyEffect: new Big(0),
investmentValues: {}, investmentValuesAccumulated: {},
investmentValuesAccumulatedWithCurrencyEffect: {},
investmentValuesWithCurrencyEffect: {}, investmentValuesWithCurrencyEffect: {},
netPerformance: new Big(0), netPerformance: new Big(0),
netPerformancePercentage: new Big(0), netPerformancePercentage: new Big(0),
@ -1639,11 +1352,15 @@ export class PortfolioCalculator {
feesWithCurrencyEffect.minus(feesAtStartDateWithCurrencyEffect) feesWithCurrencyEffect.minus(feesAtStartDateWithCurrencyEffect)
); );
investmentValues[order.date] = totalInvestment; investmentValuesAccumulated[order.date] = totalInvestment;
investmentValuesWithCurrencyEffect[order.date] = investmentValuesAccumulatedWithCurrencyEffect[order.date] =
totalInvestmentWithCurrencyEffect; totalInvestmentWithCurrencyEffect;
investmentValuesWithCurrencyEffect[order.date] = (
investmentValuesWithCurrencyEffect[order.date] ?? new Big(0)
).add(transactionInvestmentWithCurrencyEffect);
timeWeightedInvestmentValues[order.date] = timeWeightedInvestmentValues[order.date] =
totalInvestmentDays > 0 totalInvestmentDays > 0
? sumOfTimeWeightedInvestments.div(totalInvestmentDays) ? sumOfTimeWeightedInvestments.div(totalInvestmentDays)
@ -1801,7 +1518,8 @@ export class PortfolioCalculator {
grossPerformancePercentageWithCurrencyEffect, grossPerformancePercentageWithCurrencyEffect,
initialValue, initialValue,
initialValueWithCurrencyEffect, initialValueWithCurrencyEffect,
investmentValues, investmentValuesAccumulated,
investmentValuesAccumulatedWithCurrencyEffect,
investmentValuesWithCurrencyEffect, investmentValuesWithCurrencyEffect,
netPerformancePercentage, netPerformancePercentage,
netPerformancePercentageWithCurrencyEffect, netPerformancePercentageWithCurrencyEffect,
@ -1823,15 +1541,4 @@ export class PortfolioCalculator {
timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect
}; };
} }
private isNextItemActive(
timelineSpecification: TimelineSpecification[],
currentDate: Date,
i: number
) {
return (
i + 1 < timelineSpecification.length &&
!isBefore(currentDate, parseDate(timelineSpecification[i + 1].start))
);
}
} }

View File

@ -74,6 +74,11 @@ export class PortfolioController {
): Promise<PortfolioDetails & { hasError: boolean }> { ): Promise<PortfolioDetails & { hasError: boolean }> {
let hasDetails = true; let hasDetails = true;
let hasError = false; let hasError = false;
const hasReadRestrictedAccessPermission =
this.userService.hasReadRestrictedAccessPermission({
impersonationId,
user: this.request.user
});
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
hasDetails = this.request.user.subscription.type === 'Premium'; hasDetails = this.request.user.subscription.type === 'Premium';
@ -108,7 +113,7 @@ export class PortfolioController {
let portfolioSummary = summary; let portfolioSummary = summary;
if ( if (
impersonationId || hasReadRestrictedAccessPermission ||
this.userService.isRestrictedView(this.request.user) this.userService.isRestrictedView(this.request.user)
) { ) {
const totalInvestment = Object.values(holdings) const totalInvestment = Object.values(holdings)
@ -148,7 +153,7 @@ export class PortfolioController {
if ( if (
hasDetails === false || hasDetails === false ||
impersonationId || hasReadRestrictedAccessPermission ||
this.userService.isRestrictedView(this.request.user) this.userService.isRestrictedView(this.request.user)
) { ) {
portfolioSummary = nullifyValuesInObject(summary, [ portfolioSummary = nullifyValuesInObject(summary, [
@ -164,6 +169,7 @@ export class PortfolioController {
'excludedAccountsAndActivities', 'excludedAccountsAndActivities',
'fees', 'fees',
'fireWealth', 'fireWealth',
'interest',
'items', 'items',
'liabilities', 'liabilities',
'netWorth', 'netWorth',
@ -216,6 +222,12 @@ export class PortfolioController {
@Query('range') dateRange: DateRange = 'max', @Query('range') dateRange: DateRange = 'max',
@Query('tags') filterByTags?: string @Query('tags') filterByTags?: string
): Promise<PortfolioDividends> { ): Promise<PortfolioDividends> {
const hasReadRestrictedAccessPermission =
this.userService.hasReadRestrictedAccessPermission({
impersonationId,
user: this.request.user
});
const filters = this.apiService.buildFiltersFromQueryParams({ const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts, filterByAccounts,
filterByAssetClasses, filterByAssetClasses,
@ -230,7 +242,7 @@ export class PortfolioController {
}); });
if ( if (
impersonationId || hasReadRestrictedAccessPermission ||
this.userService.isRestrictedView(this.request.user) this.userService.isRestrictedView(this.request.user)
) { ) {
const maxDividend = dividends.reduce( const maxDividend = dividends.reduce(
@ -266,6 +278,12 @@ export class PortfolioController {
@Query('range') dateRange: DateRange = 'max', @Query('range') dateRange: DateRange = 'max',
@Query('tags') filterByTags?: string @Query('tags') filterByTags?: string
): Promise<PortfolioInvestments> { ): Promise<PortfolioInvestments> {
const hasReadRestrictedAccessPermission =
this.userService.hasReadRestrictedAccessPermission({
impersonationId,
user: this.request.user
});
const filters = this.apiService.buildFiltersFromQueryParams({ const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts, filterByAccounts,
filterByAssetClasses, filterByAssetClasses,
@ -281,7 +299,7 @@ export class PortfolioController {
}); });
if ( if (
impersonationId || hasReadRestrictedAccessPermission ||
this.userService.isRestrictedView(this.request.user) this.userService.isRestrictedView(this.request.user)
) { ) {
const maxInvestment = investments.reduce( const maxInvestment = investments.reduce(
@ -329,6 +347,12 @@ export class PortfolioController {
@Query('tags') filterByTags?: string, @Query('tags') filterByTags?: string,
@Query('withExcludedAccounts') withExcludedAccounts = false @Query('withExcludedAccounts') withExcludedAccounts = false
): Promise<PortfolioPerformanceResponse> { ): Promise<PortfolioPerformanceResponse> {
const hasReadRestrictedAccessPermission =
this.userService.hasReadRestrictedAccessPermission({
impersonationId,
user: this.request.user
});
const filters = this.apiService.buildFiltersFromQueryParams({ const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts, filterByAccounts,
filterByAssetClasses, filterByAssetClasses,
@ -344,7 +368,7 @@ export class PortfolioController {
}); });
if ( if (
impersonationId || hasReadRestrictedAccessPermission ||
this.request.user.Settings.settings.viewMode === 'ZEN' || this.request.user.Settings.settings.viewMode === 'ZEN' ||
this.userService.isRestrictedView(this.request.user) this.userService.isRestrictedView(this.request.user)
) { ) {

View File

@ -79,7 +79,7 @@ import {
subDays, subDays,
subYears subYears
} from 'date-fns'; } from 'date-fns';
import { isEmpty, last, sortBy, uniq, uniqBy } from 'lodash'; import { isEmpty, last, uniq, uniqBy } from 'lodash';
import { import {
HistoricalDataContainer, HistoricalDataContainer,
@ -293,77 +293,32 @@ export class PortfolioService {
portfolioCalculator.setTransactionPoints(transactionPoints); portfolioCalculator.setTransactionPoints(transactionPoints);
const { items } = await this.getChart({
dateRange,
impersonationId,
portfolioOrders,
transactionPoints,
userId,
userCurrency: this.request.user.Settings.settings.baseCurrency,
withDataDecimation: false
});
let investments: InvestmentItem[]; let investments: InvestmentItem[];
if (groupBy) { if (groupBy) {
investments = portfolioCalculator investments = portfolioCalculator.getInvestmentsByGroup({
.getInvestmentsByGroup(groupBy) groupBy,
.map((item) => { data: items
return {
date: item.date,
investment: item.investment.toNumber()
};
});
// Add investment of current group
const dateOfCurrentGroup = format(
set(new Date(), {
date: 1,
month: groupBy === 'year' ? 0 : new Date().getMonth()
}),
DATE_FORMAT
);
const investmentOfCurrentGroup = investments.filter(({ date }) => {
return date === dateOfCurrentGroup;
}); });
if (investmentOfCurrentGroup.length <= 0) {
investments.push({
date: dateOfCurrentGroup,
investment: 0
});
}
} else { } else {
investments = portfolioCalculator investments = items.map(({ date, investmentValueWithCurrencyEffect }) => {
.getInvestments() return {
.map(({ date, investment }) => { date,
return { investment: investmentValueWithCurrencyEffect
date, };
investment: investment.toNumber()
};
});
// Add investment of today
const investmentOfToday = investments.filter(({ date }) => {
return date === format(new Date(), DATE_FORMAT);
}); });
if (investmentOfToday.length <= 0) {
const pastInvestments = investments.filter(({ date }) => {
return isBefore(parseDate(date), new Date());
});
const lastInvestment = pastInvestments[pastInvestments.length - 1];
investments.push({
date: format(new Date(), DATE_FORMAT),
investment: lastInvestment?.investment ?? 0
});
}
} }
investments = sortBy(investments, ({ date }) => {
return date;
});
const startDate = this.getStartDate(
dateRange,
parseDate(investments[0]?.date)
);
investments = investments.filter(({ date }) => {
return !isBefore(parseDate(date), startDate);
});
let streaks: PortfolioInvestments['streaks']; let streaks: PortfolioInvestments['streaks'];
if (savingsRate) { if (savingsRate) {
@ -1448,7 +1403,8 @@ export class PortfolioService {
portfolioOrders, portfolioOrders,
transactionPoints, transactionPoints,
userCurrency, userCurrency,
userId userId,
withDataDecimation = true
}: { }: {
dateRange?: DateRange; dateRange?: DateRange;
impersonationId: string; impersonationId: string;
@ -1456,6 +1412,7 @@ export class PortfolioService {
transactionPoints: TransactionPoint[]; transactionPoints: TransactionPoint[];
userCurrency: string; userCurrency: string;
userId: string; userId: string;
withDataDecimation?: boolean;
}): Promise<HistoricalDataContainer> { }): Promise<HistoricalDataContainer> {
if (transactionPoints.length === 0) { if (transactionPoints.length === 0) {
return { return {
@ -1481,16 +1438,18 @@ export class PortfolioService {
const portfolioStart = parseDate(transactionPoints[0].date); const portfolioStart = parseDate(transactionPoints[0].date);
const startDate = this.getStartDate(dateRange, portfolioStart); const startDate = this.getStartDate(dateRange, portfolioStart);
const daysInMarket = differenceInDays(new Date(), startDate); let step = 1;
const step = Math.round(
daysInMarket / Math.min(daysInMarket, MAX_CHART_ITEMS)
);
const items = await portfolioCalculator.getChartData( if (withDataDecimation) {
startDate, const daysInMarket = differenceInDays(new Date(), startDate);
endDate, step = Math.round(daysInMarket / Math.min(daysInMarket, MAX_CHART_ITEMS));
step }
);
const items = await portfolioCalculator.getChartData({
step,
end: endDate,
start: startDate
});
return { return {
items, items,

View File

@ -38,6 +38,10 @@ export class UpdateUserSettingDto {
@IsOptional() @IsOptional()
emergencyFund?: number; emergencyFund?: number;
@IsArray()
@IsOptional()
'filters.accounts'?: string[];
@IsArray() @IsArray()
@IsOptional() @IsOptional()
'filters.tags'?: string[]; 'filters.tags'?: string[];

View File

@ -105,6 +105,24 @@ export class UserService {
return usersWithAdminRole.length > 0; return usersWithAdminRole.length > 0;
} }
public hasReadRestrictedAccessPermission({
impersonationId,
user
}: {
impersonationId: string;
user: UserWithSettings;
}) {
if (!impersonationId) {
return false;
}
const access = user.Access?.find(({ id }) => {
return id === impersonationId;
});
return access?.permissions?.includes('READ_RESTRICTED') ?? true;
}
public isRestrictedView(aUser: UserWithSettings) { public isRestrictedView(aUser: UserWithSettings) {
return aUser.Settings.settings.isRestrictedView ?? false; return aUser.Settings.settings.isRestrictedView ?? false;
} }
@ -113,6 +131,7 @@ export class UserService {
userWhereUniqueInput: Prisma.UserWhereUniqueInput userWhereUniqueInput: Prisma.UserWhereUniqueInput
): Promise<UserWithSettings | null> { ): Promise<UserWithSettings | null> {
const { const {
Access,
accessToken, accessToken,
Account, Account,
Analytics, Analytics,
@ -127,6 +146,7 @@ export class UserService {
updatedAt updatedAt
} = await this.prismaService.user.findUnique({ } = await this.prismaService.user.findUnique({
include: { include: {
Access: true,
Account: { Account: {
include: { Platform: true } include: { Platform: true }
}, },
@ -138,6 +158,7 @@ export class UserService {
}); });
const user: UserWithSettings = { const user: UserWithSettings = {
Access,
accessToken, accessToken,
Account, Account,
authChallenge, authChallenge,
@ -198,18 +219,18 @@ export class UserService {
new Date(), new Date(),
user.createdAt user.createdAt
); );
let frequency = 15; let frequency = 10;
if (daysSinceRegistration > 365) { if (daysSinceRegistration > 365) {
frequency = 2; frequency = 2;
} else if (daysSinceRegistration > 180) { } else if (daysSinceRegistration > 180) {
frequency = 3; frequency = 3;
} else if (daysSinceRegistration > 60) { } else if (daysSinceRegistration > 60) {
frequency = 5; frequency = 4;
} else if (daysSinceRegistration > 30) { } else if (daysSinceRegistration > 30) {
frequency = 8; frequency = 6;
} else if (daysSinceRegistration > 15) { } else if (daysSinceRegistration > 15) {
frequency = 12; frequency = 8;
} }
if (Analytics?.activityCount % frequency === 1) { if (Analytics?.activityCount % frequency === 1) {

View File

@ -230,6 +230,10 @@
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-vyzer</loc> <loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-vyzer</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-wealthfolio</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-wealthica</loc> <loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-wealthica</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -568,6 +572,10 @@
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-vyzer</loc> <loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-vyzer</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-wealthfolio</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-wealthica</loc> <loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-wealthica</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -926,6 +934,10 @@
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-vyzer</loc> <loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-vyzer</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-wealthfolio</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-wealthica</loc> <loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-wealthica</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -1130,6 +1142,10 @@
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-vyzer</loc> <loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-vyzer</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-wealthfolio</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-wealthica</loc> <loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-wealthica</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>

View File

@ -1,6 +1,7 @@
import { UserService } from '@ghostfolio/api/app/user/user.service'; import { UserService } from '@ghostfolio/api/app/user/user.service';
import { redactAttributes } from '@ghostfolio/api/helper/object.helper'; import { redactAttributes } from '@ghostfolio/api/helper/object.helper';
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
import { UserWithSettings } from '@ghostfolio/common/types';
import { import {
CallHandler, CallHandler,
ExecutionContext, ExecutionContext,
@ -22,13 +23,20 @@ export class RedactValuesInResponseInterceptor<T>
): Observable<any> { ): Observable<any> {
return next.handle().pipe( return next.handle().pipe(
map((data: any) => { map((data: any) => {
const request = context.switchToHttp().getRequest(); const { headers, user }: { headers: Headers; user: UserWithSettings } =
const hasImpersonationId = context.switchToHttp().getRequest();
!!request.headers?.[HEADER_KEY_IMPERSONATION.toLowerCase()];
const impersonationId =
headers?.[HEADER_KEY_IMPERSONATION.toLowerCase()];
const hasReadRestrictedPermission =
this.userService.hasReadRestrictedAccessPermission({
impersonationId,
user
});
if ( if (
hasImpersonationId || hasReadRestrictedPermission ||
this.userService.isRestrictedView(request.user) this.userService.isRestrictedView(user)
) { ) {
data = redactAttributes({ data = redactAttributes({
object: data, object: data,

View File

@ -24,7 +24,7 @@ export class ApiService {
const searchQuery = filterBySearchQuery?.toLowerCase(); const searchQuery = filterBySearchQuery?.toLowerCase();
const tagIds = filterByTags?.split(',') ?? []; const tagIds = filterByTags?.split(',') ?? [];
return [ const filters = [
...accountIds.map((accountId) => { ...accountIds.map((accountId) => {
return <Filter>{ return <Filter>{
id: accountId, id: accountId,
@ -43,10 +43,6 @@ export class ApiService {
type: 'ASSET_SUB_CLASS' type: 'ASSET_SUB_CLASS'
}; };
}), }),
{
id: searchQuery,
type: 'SEARCH_QUERY'
},
...tagIds.map((tagId) => { ...tagIds.map((tagId) => {
return <Filter>{ return <Filter>{
id: tagId, id: tagId,
@ -54,5 +50,14 @@ export class ApiService {
}; };
}) })
]; ];
if (searchQuery) {
filters.push({
id: searchQuery,
type: 'SEARCH_QUERY'
});
}
return filters;
} }
} }

View File

@ -62,9 +62,9 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
}, this.configurationService.get('REQUEST_TIMEOUT')); }, this.configurationService.get('REQUEST_TIMEOUT'));
return got( return got(
`${TrackinsightDataEnhancerService.baseUrl}/funds/${symbol.split( `${TrackinsightDataEnhancerService.baseUrl}/funds/${
'.' symbol.split('.')?.[0]
)?.[0]}.json`, }.json`,
{ {
// @ts-ignore // @ts-ignore
signal: abortController.signal signal: abortController.signal
@ -104,9 +104,9 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
}, this.configurationService.get('REQUEST_TIMEOUT')); }, this.configurationService.get('REQUEST_TIMEOUT'));
return got( return got(
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${symbol.split( `${TrackinsightDataEnhancerService.baseUrl}/holdings/${
'.' symbol.split('.')?.[0]
)?.[0]}.json`, }.json`,
{ {
// @ts-ignore // @ts-ignore
signal: abortController.signal signal: abortController.signal

View File

@ -1,7 +1,11 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service'; import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface'; import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
import { DEFAULT_CURRENCY, UNKNOWN_KEY } from '@ghostfolio/common/config'; import {
DEFAULT_CURRENCY,
REPLACE_NAME_PARTS,
UNKNOWN_KEY
} from '@ghostfolio/common/config';
import { isCurrency } from '@ghostfolio/common/helper'; import { isCurrency } from '@ghostfolio/common/helper';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { import {
@ -137,18 +141,11 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
if (name) { if (name) {
name = name.replace('&amp;', '&'); name = name.replace('&amp;', '&');
name = name.replace('Amundi Index Solutions - ', ''); for (const part of REPLACE_NAME_PARTS) {
name = name.replace('iShares ETF (CH) - ', ''); name = name.replace(part, '');
name = name.replace('iShares III Public Limited Company - ', ''); }
name = name.replace('iShares V PLC - ', '');
name = name.replace('iShares VI Public Limited Company - ', ''); name = name.trim();
name = name.replace('iShares VII PLC - ', '');
name = name.replace('Multi Units Luxembourg - ', '');
name = name.replace('VanEck ETFs N.V. - ', '');
name = name.replace('Vaneck Vectors Ucits Etfs Plc - ', '');
name = name.replace('Vanguard Funds Public Limited Company - ', '');
name = name.replace('Vanguard Index Funds - ', '');
name = name.replace('Xtrackers (IE) Plc - ', '');
} }
if (quoteType === 'FUTURE') { if (quoteType === 'FUTURE') {

View File

@ -11,7 +11,10 @@ import {
IDataProviderHistoricalResponse, IDataProviderHistoricalResponse,
IDataProviderResponse IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config'; import {
DEFAULT_CURRENCY,
REPLACE_NAME_PARTS
} from '@ghostfolio/common/config';
import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper'; import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { import {
@ -22,6 +25,7 @@ import {
} from '@prisma/client'; } from '@prisma/client';
import { addDays, format, isSameDay, isToday } from 'date-fns'; import { addDays, format, isSameDay, isToday } from 'date-fns';
import got from 'got'; import got from 'got';
import { isNumber } from 'lodash';
@Injectable() @Injectable()
export class EodHistoricalDataService implements DataProviderInterface { export class EodHistoricalDataService implements DataProviderInterface {
@ -144,10 +148,17 @@ export class EodHistoricalDataService implements DataProviderInterface {
).json<any>(); ).json<any>();
return response.reduce( return response.reduce(
(result, historicalItem, index, array) => { (result, { close, date }, index, array) => {
result[this.convertFromEodSymbol(symbol)][historicalItem.date] = { if (isNumber(close)) {
marketPrice: historicalItem.close result[this.convertFromEodSymbol(symbol)][date] = {
}; marketPrice: close
};
} else {
Logger.error(
`Could not get historical market data for ${symbol} (${this.getName()}) at ${date}`,
'EodHistoricalDataService'
);
}
return result; return result;
}, },
@ -232,14 +243,23 @@ export class EodHistoricalDataService implements DataProviderInterface {
return lookupItem.symbol === code; return lookupItem.symbol === code;
})?.currency; })?.currency;
result[this.convertFromEodSymbol(code)] = { if (isNumber(close)) {
currency: result[this.convertFromEodSymbol(code)] = {
currency ?? currency:
this.convertFromEodSymbol(code)?.replace(DEFAULT_CURRENCY, ''), currency ??
dataSource: DataSource.EOD_HISTORICAL_DATA, this.convertFromEodSymbol(code)?.replace(DEFAULT_CURRENCY, ''),
marketPrice: close, dataSource: this.getName(),
marketState: isToday(new Date(timestamp * 1000)) ? 'open' : 'closed' marketPrice: close,
}; marketState: isToday(new Date(timestamp * 1000))
? 'open'
: 'closed'
};
} else {
Logger.error(
`Could not get quote for ${this.convertFromEodSymbol(code)} (${this.getName()})`,
'EodHistoricalDataService'
);
}
return result; return result;
}, },
@ -345,6 +365,18 @@ export class EodHistoricalDataService implements DataProviderInterface {
return aSymbol; return aSymbol;
} }
private formatName({ name }: { name: string }) {
if (name) {
for (const part of REPLACE_NAME_PARTS) {
name = name.replace(part, '');
}
name = name.trim();
}
return name;
}
private async getSearchResult(aQuery: string): Promise< private async getSearchResult(aQuery: string): Promise<
(LookupItem & { (LookupItem & {
assetClass: AssetClass; assetClass: AssetClass;
@ -380,9 +412,9 @@ export class EodHistoricalDataService implements DataProviderInterface {
assetClass, assetClass,
assetSubClass, assetSubClass,
isin, isin,
name,
currency: this.convertCurrency(Currency), currency: this.convertCurrency(Currency),
dataSource: this.getName(), dataSource: this.getName(),
name: this.formatName({ name }),
symbol: `${Code}.${Exchange}` symbol: `${Code}.${Exchange}`
}; };
} }

View File

@ -42,10 +42,21 @@ export class ManualService implements DataProviderInterface {
public async getAssetProfile( public async getAssetProfile(
aSymbol: string aSymbol: string
): Promise<Partial<SymbolProfile>> { ): Promise<Partial<SymbolProfile>> {
return { const assetProfile: Partial<SymbolProfile> = {
dataSource: this.getName(), dataSource: this.getName(),
symbol: aSymbol symbol: aSymbol
}; };
const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles([
{ dataSource: this.getName(), symbol: aSymbol }
]);
if (symbolProfile) {
assetProfile.currency = symbolProfile.currency;
assetProfile.name = symbolProfile.name;
}
return assetProfile;
} }
public async getDividends({}: GetDividendsParams) { public async getDividends({}: GetDividendsParams) {

View File

@ -38,7 +38,7 @@
[pageTitle]="pageTitle" [pageTitle]="pageTitle"
[user]="user" [user]="user"
(signOut)="onSignOut()" (signOut)="onSignOut()"
></gf-header> />
</header> </header>
<main role="main"> <main role="main">

View File

@ -17,8 +17,13 @@
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Permission</th> <th *matHeaderCellDef class="px-1" i18n mat-header-cell>Permission</th>
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell> <td *matCellDef="let element" class="px-1 text-nowrap" mat-cell>
<div class="align-items-center d-flex"> <div class="align-items-center d-flex">
<ion-icon class="mr-1" name="lock-closed-outline" /> @if (element.permissions.includes('READ')) {
<ng-container i18n>Restricted View</ng-container> <ion-icon class="mr-1" name="lock-open-outline" />
<ng-container i18n>View</ng-container>
} @else if (element.permissions.includes('READ_RESTRICTED')) {
<ion-icon class="mr-1" name="lock-closed-outline" />
<ng-container i18n>Restricted view</ng-container>
}
</div> </div>
</td> </td>
</ng-container> </ng-container>

View File

@ -178,7 +178,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
} }
this.dataService this.dataService
.fetchExport(activityIds) .fetchExport({ activityIds })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((data) => { .subscribe((data) => {
downloadAsFile({ downloadAsFile({

View File

@ -4,7 +4,7 @@
[deviceType]="data.deviceType" [deviceType]="data.deviceType"
[title]="name" [title]="name"
(closeButtonClicked)="onClose()" (closeButtonClicked)="onClose()"
></gf-dialog-header> />
<div class="flex-grow-1" mat-dialog-content> <div class="flex-grow-1" mat-dialog-content>
<div class="container p-0"> <div class="container p-0">
@ -16,7 +16,7 @@
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency" [unit]="user?.settings?.baseCurrency"
[value]="valueInBaseCurrency" [value]="valueInBaseCurrency"
></gf-value> />
</div> </div>
</div> </div>
@ -28,7 +28,7 @@
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView" [isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[isLoading]="isLoadingChart" [isLoading]="isLoadingChart"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
></gf-investment-chart> />
</div> </div>
<div class="mb-3 row"> <div class="mb-3 row">
@ -79,7 +79,7 @@
[deviceType]="data.deviceType" [deviceType]="data.deviceType"
[holdings]="holdings" [holdings]="holdings"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
></gf-holdings-table> />
</mat-tab> </mat-tab>
<mat-tab> <mat-tab>
<ng-template mat-tab-label> <ng-template mat-tab-label>
@ -102,7 +102,7 @@
[totalItems]="totalItems" [totalItems]="totalItems"
(export)="onExport()" (export)="onExport()"
(sortChanged)="onSortChanged($event)" (sortChanged)="onSortChanged($event)"
></gf-activities-table-lazy> />
<gf-activities-table <gf-activities-table
*ngIf="user?.settings?.isExperimentalFeatures !== true" *ngIf="user?.settings?.isExperimentalFeatures !== true"
[activities]="activities" [activities]="activities"
@ -115,7 +115,7 @@
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[showActions]="false" [showActions]="false"
(export)="onExport()" (export)="onExport()"
></gf-activities-table> />
</mat-tab> </mat-tab>
<mat-tab> <mat-tab>
<ng-template mat-tab-label> <ng-template mat-tab-label>
@ -128,7 +128,7 @@
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[showActions]="!hasImpersonationId && hasPermissionToDeleteAccountBalance && !user.settings.isRestrictedView" [showActions]="!hasImpersonationId && hasPermissionToDeleteAccountBalance && !user.settings.isRestrictedView"
(accountBalanceDeleted)="onDeleteAccountBalance($event)" (accountBalanceDeleted)="onDeleteAccountBalance($event)"
></gf-account-balances> />
</mat-tab> </mat-tab>
</mat-tab-group> </mat-tab-group>
</div> </div>
@ -138,4 +138,4 @@
mat-dialog-actions mat-dialog-actions
[deviceType]="data.deviceType" [deviceType]="data.deviceType"
(closeButtonClicked)="onClose()" (closeButtonClicked)="onClose()"
></gf-dialog-footer> />

View File

@ -39,7 +39,7 @@
class="d-inline d-sm-none mr-1" class="d-inline d-sm-none mr-1"
[tooltip]="element.Platform?.name" [tooltip]="element.Platform?.name"
[url]="element.Platform?.url" [url]="element.Platform?.url"
></gf-symbol-icon> />
<span>{{ element.name }} </span> <span>{{ element.name }} </span>
<span <span
*ngIf="element.isDefault" *ngIf="element.isDefault"
@ -83,7 +83,7 @@
class="mr-1" class="mr-1"
[tooltip]="element.Platform?.name" [tooltip]="element.Platform?.name"
[url]="element.Platform?.url" [url]="element.Platform?.url"
></gf-symbol-icon> />
<span>{{ element.Platform?.name }}</span> <span>{{ element.Platform?.name }}</span>
</div> </div>
</td> </td>
@ -131,7 +131,7 @@
[isCurrency]="true" [isCurrency]="true"
[locale]="locale" [locale]="locale"
[value]="element.balance" [value]="element.balance"
></gf-value> />
</td> </td>
<td <td
*matFooterCellDef *matFooterCellDef
@ -143,7 +143,7 @@
[isCurrency]="true" [isCurrency]="true"
[locale]="locale" [locale]="locale"
[value]="totalBalanceInBaseCurrency" [value]="totalBalanceInBaseCurrency"
></gf-value> />
</td> </td>
</ng-container> </ng-container>
@ -166,7 +166,7 @@
[isCurrency]="true" [isCurrency]="true"
[locale]="locale" [locale]="locale"
[value]="element.value" [value]="element.value"
></gf-value> />
</td> </td>
<td <td
*matFooterCellDef *matFooterCellDef
@ -178,7 +178,7 @@
[isCurrency]="true" [isCurrency]="true"
[locale]="locale" [locale]="locale"
[value]="totalValueInBaseCurrency" [value]="totalValueInBaseCurrency"
></gf-value> />
</td> </td>
</ng-container> </ng-container>
@ -201,7 +201,7 @@
[isCurrency]="true" [isCurrency]="true"
[locale]="locale" [locale]="locale"
[value]="element.valueInBaseCurrency" [value]="element.valueInBaseCurrency"
></gf-value> />
</td> </td>
<td <td
*matFooterCellDef *matFooterCellDef
@ -213,7 +213,7 @@
[isCurrency]="true" [isCurrency]="true"
[locale]="locale" [locale]="locale"
[value]="totalValueInBaseCurrency" [value]="totalValueInBaseCurrency"
></gf-value> />
</td> </td>
</ng-container> </ng-container>
@ -296,4 +296,4 @@
height: '1.5rem', height: '1.5rem',
width: '100%' width: '100%'
}" }"
></ngx-skeleton-loader> />

View File

@ -8,7 +8,7 @@
[showXAxis]="true" [showXAxis]="true"
[showYAxis]="true" [showYAxis]="true"
[symbol]="symbol" [symbol]="symbol"
></gf-line-chart> />
<div <div
*ngFor="let itemByMonth of marketDataByMonth | keyvalue" *ngFor="let itemByMonth of marketDataByMonth | keyvalue"
class="d-flex" class="d-flex"

View File

@ -6,7 +6,7 @@
[isLoading]="isLoading" [isLoading]="isLoading"
[placeholder]="placeholder" [placeholder]="placeholder"
(valueChanged)="filters$.next($event)" (valueChanged)="filters$.next($event)"
></gf-activities-filter> />
</div> </div>
</div> </div>
<div class="row"> <div class="row">
@ -213,7 +213,7 @@
height: '1.5rem', height: '1.5rem',
width: '100%' width: '100%'
}" }"
></ngx-skeleton-loader> />
</div> </div>
</div> </div>

View File

@ -50,7 +50,7 @@
[marketData]="marketDataDetails" [marketData]="marketDataDetails"
[symbol]="data.symbol" [symbol]="data.symbol"
(marketDataChanged)="onMarketDataChanged($event)" (marketDataChanged)="onMarketDataChanged($event)"
></gf-admin-market-data-detail> />
<div class="mt-3" formGroupName="historicalData"> <div class="mt-3" formGroupName="historicalData">
<mat-form-field appearance="outline" class="w-100 without-hint"> <mat-form-field appearance="outline" class="w-100 without-hint">
@ -162,7 +162,7 @@
[keys]="['name']" [keys]="['name']"
[maxItems]="10" [maxItems]="10"
[positions]="sectors" [positions]="sectors"
></gf-portfolio-proportion-chart> />
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<div class="h5" i18n>Countries</div> <div class="h5" i18n>Countries</div>
@ -172,7 +172,7 @@
[keys]="['name']" [keys]="['name']"
[maxItems]="10" [maxItems]="10"
[positions]="countries" [positions]="countries"
></gf-portfolio-proportion-chart> />
</div> </div>
</ng-template> </ng-template>
</ng-container> </ng-container>

View File

@ -16,7 +16,7 @@
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[precision]="0" [precision]="0"
[value]="userCount" [value]="userCount"
></gf-value> />
</div> </div>
</div> </div>
<div class="d-flex my-3"> <div class="d-flex my-3">
@ -26,7 +26,7 @@
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[precision]="0" [precision]="0"
[value]="transactionCount" [value]="transactionCount"
></gf-value> />
<div *ngIf="transactionCount && userCount"> <div *ngIf="transactionCount && userCount">
{{ transactionCount / userCount | number : '1.2-2' }} {{ transactionCount / userCount | number : '1.2-2' }}
<span i18n>per User</span> <span i18n>per User</span>
@ -39,10 +39,7 @@
<table> <table>
<tr *ngFor="let exchangeRate of exchangeRates"> <tr *ngFor="let exchangeRate of exchangeRates">
<td> <td>
<gf-value <gf-value [locale]="user?.settings?.locale" [value]="1" />
[locale]="user?.settings?.locale"
[value]="1"
></gf-value>
</td> </td>
<td class="pl-1">{{ exchangeRate.label1 }}</td> <td class="pl-1">{{ exchangeRate.label1 }}</td>
<td class="px-1">=</td> <td class="px-1">=</td>
@ -52,7 +49,7 @@
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[precision]="4" [precision]="4"
[value]="exchangeRate.value" [value]="exchangeRate.value"
></gf-value> />
</td> </td>
<td class="pl-1">{{ exchangeRate.label2 }}</td> <td class="pl-1">{{ exchangeRate.label2 }}</td>
<td> <td>

View File

@ -35,7 +35,7 @@
class="d-inline mr-1" class="d-inline mr-1"
[tooltip]="element.name" [tooltip]="element.name"
[url]="element.url" [url]="element.url"
></gf-symbol-icon> />
<span>{{ element.name }}</span> <span>{{ element.name }}</span>
</td></ng-container </td></ng-container
> >

View File

@ -46,7 +46,7 @@
class="ml-1" class="ml-1"
[enableLink]="false" [enableLink]="false"
[title]="'Expires ' + formatDistanceToNow(element.subscription.expiresAt) + ' (' + (element.subscription.expiresAt | date: defaultDateFormat) + ')'" [title]="'Expires ' + formatDistanceToNow(element.subscription.expiresAt) + ' (' + (element.subscription.expiresAt | date: defaultDateFormat) + ')'"
></gf-premium-indicator> />
</div> </div>
</td> </td>
</ng-container> </ng-container>
@ -107,7 +107,7 @@
class="d-inline-block justify-content-end" class="d-inline-block justify-content-end"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[value]="element.accountCount" [value]="element.accountCount"
></gf-value> />
</td> </td>
</ng-container> </ng-container>
@ -128,7 +128,7 @@
class="d-inline-block justify-content-end" class="d-inline-block justify-content-end"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[value]="element.transactionCount" [value]="element.transactionCount"
></gf-value> />
</td> </td>
</ng-container> </ng-container>
@ -153,7 +153,7 @@
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[precision]="0" [precision]="0"
[value]="element.engagement" [value]="element.engagement"
></gf-value> />
</td> </td>
</ng-container> </ng-container>

View File

@ -7,7 +7,7 @@
<gf-premium-indicator <gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'" *ngIf="user?.subscription?.type === 'Basic'"
class="ml-1" class="ml-1"
></gf-premium-indicator> />
</div> </div>
</div> </div>
<div class="col-md-6 col-xs-12 d-flex justify-content-end"> <div class="col-md-6 col-xs-12 d-flex justify-content-end">
@ -50,7 +50,7 @@
height: '100%', height: '100%',
width: '100%' width: '100%'
}" }"
></ngx-skeleton-loader> />
<canvas <canvas
#chartCanvas #chartCanvas
class="h-100" class="h-100"

View File

@ -19,5 +19,5 @@
[theme]="{ [theme]="{
height: '100%' height: '100%'
}" }"
></ngx-skeleton-loader> />
</div> </div>

View File

@ -7,7 +7,7 @@
[ngClass]="{ 'w-100': hasTabs }" [ngClass]="{ 'w-100': hasTabs }"
[routerLink]="['/']" [routerLink]="['/']"
> >
<gf-logo class="px-2" [label]="pageTitle"></gf-logo> <gf-logo class="px-2" [label]="pageTitle" />
</a> </a>
</div> </div>
<span class="spacer"></span> <span class="spacer"></span>
@ -141,7 +141,7 @@
[user]="user" [user]="user"
(closed)="closeAssistant()" (closed)="closeAssistant()"
(dateRangeChanged)="onDateRangeChange($event)" (dateRangeChanged)="onDateRangeChange($event)"
(selectedTagChanged)="onSelectedTagChanged($event)" (filtersChanged)="onFiltersChanged($event)"
/> />
</mat-menu> </mat-menu>
</li> </li>
@ -165,6 +165,32 @@
/> />
</button> </button>
<mat-menu #accountMenu="matMenu" xPosition="before"> <mat-menu #accountMenu="matMenu" xPosition="before">
<ng-container
*ngIf="
hasPermissionForSubscription &&
user?.subscription?.type === 'Basic'
"
>
<a class="d-flex" mat-menu-item [routerLink]="routerLinkPricing"
><span class="align-items-center d-flex"
><span
><ng-container
*ngIf="user.subscription.offer === 'default'"
i18n
>Upgrade Plan</ng-container
>
<ng-container
*ngIf="user.subscription.offer === 'renewal'"
i18n
>Renew Plan</ng-container
></span
>
<gf-premium-indicator
class="d-inline-block ml-1"
[enableLink]="false" /></span
></a>
<hr class="m-0" />
</ng-container>
<ng-container *ngIf="user?.access?.length > 0"> <ng-container *ngIf="user?.access?.length > 0">
<button mat-menu-item (click)="impersonateAccount(null)"> <button mat-menu-item (click)="impersonateAccount(null)">
<span class="align-items-center d-flex"> <span class="align-items-center d-flex">
@ -295,7 +321,7 @@
class="px-2" class="px-2"
[label]="pageTitle" [label]="pageTitle"
[showLabel]="currentRoute !== 'register'" [showLabel]="currentRoute !== 'register'"
></gf-logo> />
</a> </a>
</div> </div>
<span class="spacer"></span> <span class="spacer"></span>

View File

@ -11,6 +11,7 @@ import {
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { MatMenuTrigger } from '@angular/material/menu'; import { MatMenuTrigger } from '@angular/material/menu';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto';
import { LoginWithAccessTokenDialog } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.component'; import { LoginWithAccessTokenDialog } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.component';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
@ -20,11 +21,10 @@ import {
} from '@ghostfolio/client/services/settings-storage.service'; } from '@ghostfolio/client/services/settings-storage.service';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { InfoItem, User } from '@ghostfolio/common/interfaces'; import { Filter, InfoItem, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { DateRange } from '@ghostfolio/common/types'; import { DateRange } from '@ghostfolio/common/types';
import { AssistantComponent } from '@ghostfolio/ui/assistant/assistant.component'; import { AssistantComponent } from '@ghostfolio/ui/assistant/assistant.component';
import { Tag } from '@prisma/client';
import { EMPTY, Subject } from 'rxjs'; import { EMPTY, Subject } from 'rxjs';
import { catchError, takeUntil } from 'rxjs/operators'; import { catchError, takeUntil } from 'rxjs/operators';
@ -162,6 +162,34 @@ export class HeaderComponent implements OnChanges {
}); });
} }
public onFiltersChanged(filters: Filter[]) {
const userSetting: UpdateUserSettingDto = {};
for (const filter of filters) {
let filtersType: string;
if (filter.type === 'ACCOUNT') {
filtersType = 'accounts';
} else if (filter.type === 'TAG') {
filtersType = 'tags';
}
userSetting[`filters.${filtersType}`] = filter.id ? [filter.id] : null;
}
this.dataService
.putUserSetting(userSetting)
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.userService.remove();
this.userService
.get()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe();
});
}
public onMenuClosed() { public onMenuClosed() {
this.isMenuOpen = false; this.isMenuOpen = false;
} }
@ -174,20 +202,6 @@ export class HeaderComponent implements OnChanges {
this.assistantElement.initialize(); this.assistantElement.initialize();
} }
public onSelectedTagChanged(tag: Tag) {
this.dataService
.putUserSetting({ 'filters.tags': tag ? [tag.id] : null })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.userService.remove();
this.userService
.get()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe();
});
}
public onSignOut() { public onSignOut() {
this.signOut.next(); this.signOut.next();
} }

View File

@ -7,6 +7,7 @@ import { RouterModule } from '@angular/router';
import { LoginWithAccessTokenDialogModule } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.module'; import { LoginWithAccessTokenDialogModule } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.module';
import { GfAssistantModule } from '@ghostfolio/ui/assistant'; import { GfAssistantModule } from '@ghostfolio/ui/assistant';
import { GfLogoModule } from '@ghostfolio/ui/logo'; import { GfLogoModule } from '@ghostfolio/ui/logo';
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
import { HeaderComponent } from './header.component'; import { HeaderComponent } from './header.component';
@ -17,6 +18,7 @@ import { HeaderComponent } from './header.component';
CommonModule, CommonModule,
GfAssistantModule, GfAssistantModule,
GfLogoModule, GfLogoModule,
GfPremiumIndicatorModule,
LoginWithAccessTokenDialogModule, LoginWithAccessTokenDialogModule,
MatButtonModule, MatButtonModule,
MatMenuModule, MatMenuModule,

View File

@ -5,7 +5,7 @@
[isLoading]="positions === undefined" [isLoading]="positions === undefined"
[options]="dateRangeOptions" [options]="dateRangeOptions"
(change)="onChangeDateRange($event.value)" (change)="onChangeDateRange($event.value)"
></gf-toggle> />
</div> </div>
<div class="row"> <div class="row">
<div class="align-items-center col-xs-12 col-md-8 offset-md-2"> <div class="align-items-center col-xs-12 col-md-8 offset-md-2">
@ -18,7 +18,7 @@
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[positions]="positions" [positions]="positions"
[range]="user?.settings?.dateRange" [range]="user?.settings?.dateRange"
></gf-positions> />
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
<div *ngIf="hasPermissionToCreateOrder" class="text-center"> <div *ngIf="hasPermissionToCreateOrder" class="text-center">

View File

@ -18,11 +18,11 @@
[yMaxLabel]="greedLabel" [yMaxLabel]="greedLabel"
[yMin]="0" [yMin]="0"
[yMinLabel]="fearLabel" [yMinLabel]="fearLabel"
></gf-line-chart> />
<gf-fear-and-greed-index <gf-fear-and-greed-index
class="d-flex justify-content-center" class="d-flex justify-content-center"
[fearAndGreedIndex]="fearAndGreedIndex" [fearAndGreedIndex]="fearAndGreedIndex"
></gf-fear-and-greed-index> />
</div> </div>
</div> </div>
@ -32,7 +32,7 @@
[benchmarks]="benchmarks" [benchmarks]="benchmarks"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[user]="user" [user]="user"
></gf-benchmark> />
<ngx-skeleton-loader <ngx-skeleton-loader
*ngIf="isLoading" *ngIf="isLoading"
animation="pulse" animation="pulse"
@ -41,7 +41,7 @@
height: '1.5rem', height: '1.5rem',
width: '100%' width: '100%'
}" }"
></ngx-skeleton-loader> />
</div> </div>
</div> </div>
</div> </div>

View File

@ -74,7 +74,6 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
}); });
this.showDetails = this.showDetails =
!this.hasImpersonationId &&
!this.user.settings.isRestrictedView && !this.user.settings.isRestrictedView &&
this.user.settings.viewMode !== 'ZEN'; this.user.settings.viewMode !== 'ZEN';

View File

@ -78,7 +78,7 @@
[showLoader]="false" [showLoader]="false"
[showXAxis]="false" [showXAxis]="false"
[showYAxis]="false" [showYAxis]="false"
></gf-line-chart> />
</div> </div>
</div> </div>
</div> </div>
@ -95,7 +95,7 @@
[performance]="performance" [performance]="performance"
[showDetails]="showDetails" [showDetails]="showDetails"
[unit]="unit" [unit]="unit"
></gf-portfolio-performance> />
<div <div
*ngIf="showDetails && !user?.settings?.isExperimentalFeatures" *ngIf="showDetails && !user?.settings?.isExperimentalFeatures"
class="text-center" class="text-center"
@ -105,7 +105,7 @@
[isLoading]="isLoadingPerformance" [isLoading]="isLoadingPerformance"
[options]="dateRangeOptions" [options]="dateRangeOptions"
(change)="onChangeDateRange($event.value)" (change)="onChangeDateRange($event.value)"
></gf-toggle> />
</div> </div>
</div> </div>
</div> </div>

View File

@ -12,7 +12,7 @@
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[summary]="summary" [summary]="summary"
(emergencyFundChanged)="onChangeEmergencyFund($event)" (emergencyFundChanged)="onChangeEmergencyFund($event)"
></gf-portfolio-summary> />
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
</div> </div>

View File

@ -5,7 +5,7 @@
height: '100%', height: '100%',
width: '100%' width: '100%'
}" }"
></ngx-skeleton-loader> />
<canvas <canvas
#chartCanvas #chartCanvas
class="h-100" class="h-100"

View File

@ -2,7 +2,7 @@
mat-dialog-title mat-dialog-title
[title]="data.title" [title]="data.title"
(closeButtonClicked)="onClose()" (closeButtonClicked)="onClose()"
></gf-dialog-header> />
<div class="py-3" mat-dialog-content> <div class="py-3" mat-dialog-content>
<div class="align-items-center d-flex flex-column"> <div class="align-items-center d-flex flex-column">

View File

@ -1,14 +1,12 @@
<div class="container p-0"> <div class="container p-0">
<div class="no-gutters row"> <div class="no-gutters row">
<div <div class="status-container text-muted text-right">
class="status-container text-muted text-right"
(click)="onShowErrors()"
>
@if (errors?.length > 0 && !isLoading) { @if (errors?.length > 0 && !isLoading) {
<ion-icon <ion-icon
i18n-title i18n-title
name="time-outline" name="time-outline"
title="Oops! Our data provider partner is experiencing the hiccups." title="Oops! A data provider is experiencing the hiccups."
(click)="onShowErrors()"
/> />
} }
</div> </div>
@ -20,7 +18,7 @@
height: '4rem', height: '4rem',
width: '15rem' width: '15rem'
}" }"
></ngx-skeleton-loader> />
</div> </div>
<div <div
class="display-4 font-weight-bold m-0 text-center value-container" class="display-4 font-weight-bold m-0 text-center value-container"
@ -43,7 +41,7 @@
[isCurrency]="true" [isCurrency]="true"
[locale]="locale" [locale]="locale"
[value]="isLoading ? undefined : performance?.currentNetPerformance" [value]="isLoading ? undefined : performance?.currentNetPerformance"
></gf-value> />
</div> </div>
<div class="col"> <div class="col">
<gf-value <gf-value
@ -53,7 +51,7 @@
[value]=" [value]="
isLoading ? undefined : performance?.currentNetPerformancePercent isLoading ? undefined : performance?.currentNetPerformancePercent
" "
></gf-value> />
</div> </div>
</div> </div>
</div> </div>

View File

@ -58,7 +58,7 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
duration: 1, duration: 1,
separator: getNumberFormatGroup(this.locale) separator: getNumberFormatGroup(this.locale)
}).start(); }).start();
} else if (this.performance?.currentValue === null) { } else if (this.showDetails === false) {
new CountUp( new CountUp(
'value', 'value',
this.performance?.currentNetPerformancePercent * 100, this.performance?.currentNetPerformancePercent * 100,
@ -69,6 +69,8 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
separator: getNumberFormatGroup(this.locale) separator: getNumberFormatGroup(this.locale)
} }
).start(); ).start();
} else {
this.value.nativeElement.innerHTML = '*****';
} }
} }
} }

View File

@ -2,7 +2,7 @@
<div class="flex-nowrap px-3 py-1 row"> <div class="flex-nowrap px-3 py-1 row">
<div class="flex-grow-1 text-truncate" i18n>Time in Market</div> <div class="flex-grow-1 text-truncate" i18n>Time in Market</div>
<div class="justify-content-end"> <div class="justify-content-end">
<gf-value class="justify-content-end" [value]="timeInMarket"></gf-value> <gf-value class="justify-content-end" [value]="timeInMarket" />
</div> </div>
</div> </div>
<div <div
@ -10,8 +10,8 @@
[hidden]="summary?.ordersCount === null" [hidden]="summary?.ordersCount === null"
> >
<div class="flex-grow-1 ml-3 text-truncate" i18n> <div class="flex-grow-1 ml-3 text-truncate" i18n>
{{ summary?.ordersCount }} {summary?.ordersCount, plural, =1 {transaction} {{ summary?.ordersCount }}
other {transactions}} {summary?.ordersCount, plural, =1 {transaction} other {transactions}}
</div> </div>
</div> </div>
<div class="row"> <div class="row">
@ -26,7 +26,7 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.totalBuy" [value]="isLoading ? undefined : summary?.totalBuy"
></gf-value> />
</div> </div>
</div> </div>
<div class="flex-nowrap px-3 py-1 row"> <div class="flex-nowrap px-3 py-1 row">
@ -38,7 +38,7 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.totalSell" [value]="isLoading ? undefined : summary?.totalSell"
></gf-value> />
</div> </div>
</div> </div>
<div class="row"> <div class="row">
@ -53,7 +53,7 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.committedFunds" [value]="isLoading ? undefined : summary?.committedFunds"
></gf-value> />
</div> </div>
</div> </div>
<div class="flex-nowrap px-3 py-1 row"> <div class="flex-nowrap px-3 py-1 row">
@ -65,13 +65,17 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.currentGrossPerformance" [value]="isLoading ? undefined : summary?.currentGrossPerformance"
></gf-value> />
</div> </div>
</div> </div>
<div class="flex-nowrap px-3 py-1 row"> <div class="flex-nowrap px-3 py-1 row">
<div class="align-items-center d-flex flex-grow-1 ml-3 text-truncate"> <div class="align-items-center d-flex flex-grow-1 ml-3 text-truncate">
<ng-container i18n>Gross Performance</ng-container> <ng-container i18n>Gross Performance</ng-container>
<abbr class="initialism ml-2 text-muted" title="Time-Weighted Rate of Return">(TWR)</abbr> <abbr
class="initialism ml-2 text-muted"
title="Time-Weighted Rate of Return"
>(TWR)</abbr
>
</div> </div>
<div class="flex-column flex-wrap justify-content-end"> <div class="flex-column flex-wrap justify-content-end">
<gf-value <gf-value
@ -83,7 +87,7 @@
[value]=" [value]="
isLoading ? undefined : summary?.currentGrossPerformancePercent isLoading ? undefined : summary?.currentGrossPerformancePercent
" "
></gf-value> />
</div> </div>
</div> </div>
<div class="flex-nowrap px-3 py-1 row"> <div class="flex-nowrap px-3 py-1 row">
@ -96,7 +100,7 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.fees" [value]="isLoading ? undefined : summary?.fees"
></gf-value> />
</div> </div>
</div> </div>
<div class="row"> <div class="row">
@ -111,13 +115,17 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.currentNetPerformance" [value]="isLoading ? undefined : summary?.currentNetPerformance"
></gf-value> />
</div> </div>
</div> </div>
<div class="flex-nowrap px-3 py-1 row"> <div class="flex-nowrap px-3 py-1 row">
<div class="flex-grow-1 text-truncate ml-3"> <div class="flex-grow-1 text-truncate ml-3">
<ng-container i18n>Net Performance</ng-container> <ng-container i18n>Net Performance</ng-container>
<abbr class="initialism ml-2 text-muted" title="Time-Weighted Rate of Return">(TWR)</abbr> <abbr
class="initialism ml-2 text-muted"
title="Time-Weighted Rate of Return"
>(TWR)</abbr
>
</div> </div>
<div class="flex-column flex-wrap justify-content-end"> <div class="flex-column flex-wrap justify-content-end">
<gf-value <gf-value
@ -127,7 +135,7 @@
[isPercent]="true" [isPercent]="true"
[locale]="locale" [locale]="locale"
[value]="isLoading ? undefined : summary?.currentNetPerformancePercent" [value]="isLoading ? undefined : summary?.currentNetPerformancePercent"
></gf-value> />
</div> </div>
</div> </div>
<div class="row"> <div class="row">
@ -143,7 +151,7 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.currentValue" [value]="isLoading ? undefined : summary?.currentValue"
></gf-value> />
</div> </div>
</div> </div>
<div class="flex-nowrap px-3 py-1 row"> <div class="flex-nowrap px-3 py-1 row">
@ -155,7 +163,7 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.items" [value]="isLoading ? undefined : summary?.items"
></gf-value> />
</div> </div>
</div> </div>
<div class="flex-nowrap px-3 py-1 row"> <div class="flex-nowrap px-3 py-1 row">
@ -176,7 +184,7 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.emergencyFund?.total" [value]="isLoading ? undefined : summary?.emergencyFund?.total"
></gf-value> />
</div> </div>
</div> </div>
<div class="flex-nowrap px-3 py-1 row"> <div class="flex-nowrap px-3 py-1 row">
@ -189,7 +197,7 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.emergencyFund?.cash" [value]="isLoading ? undefined : summary?.emergencyFund?.cash"
></gf-value> />
</div> </div>
</div> </div>
<div class="flex-nowrap px-3 py-1 row"> <div class="flex-nowrap px-3 py-1 row">
@ -202,7 +210,7 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.emergencyFund?.assets" [value]="isLoading ? undefined : summary?.emergencyFund?.assets"
></gf-value> />
</div> </div>
</div> </div>
<div class="flex-nowrap px-3 py-1 row"> <div class="flex-nowrap px-3 py-1 row">
@ -214,7 +222,7 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.cash" [value]="isLoading ? undefined : summary?.cash"
></gf-value> />
</div> </div>
</div> </div>
<div class="flex-nowrap px-3 py-1 row"> <div class="flex-nowrap px-3 py-1 row">
@ -226,7 +234,7 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.excludedAccountsAndActivities" [value]="isLoading ? undefined : summary?.excludedAccountsAndActivities"
></gf-value> />
</div> </div>
</div> </div>
<div class="row"> <div class="row">
@ -246,7 +254,7 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.liabilities" [value]="isLoading ? undefined : summary?.liabilities"
></gf-value> />
</div> </div>
</div> </div>
<div class="row"> <div class="row">
@ -261,7 +269,7 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.netWorth" [value]="isLoading ? undefined : summary?.netWorth"
></gf-value> />
</div> </div>
</div> </div>
<div class="flex-nowrap px-3 py-1 row"> <div class="flex-nowrap px-3 py-1 row">
@ -276,7 +284,7 @@
[isPercent]="true" [isPercent]="true"
[locale]="locale" [locale]="locale"
[value]="isLoading ? undefined : summary?.annualizedPerformancePercent" [value]="isLoading ? undefined : summary?.annualizedPerformancePercent"
></gf-value> />
</div> </div>
</div> </div>
<div class="row"> <div class="row">
@ -291,7 +299,7 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.interest" [value]="isLoading ? undefined : summary?.interest"
></gf-value> />
</div> </div>
</div> </div>
<div class="flex-nowrap px-3 py-1 row"> <div class="flex-nowrap px-3 py-1 row">
@ -303,7 +311,7 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.dividend" [value]="isLoading ? undefined : summary?.dividend"
></gf-value> />
</div> </div>
</div> </div>
</div> </div>

View File

@ -281,7 +281,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
} }
this.dataService this.dataService
.fetchExport(activityIds) .fetchExport({ activityIds })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((data) => { .subscribe((data) => {
downloadAsFile({ downloadAsFile({

View File

@ -4,7 +4,7 @@
[deviceType]="data.deviceType" [deviceType]="data.deviceType"
[title]="SymbolProfile?.name ?? SymbolProfile?.symbol" [title]="SymbolProfile?.name ?? SymbolProfile?.symbol"
(closeButtonClicked)="onClose()" (closeButtonClicked)="onClose()"
></gf-dialog-header> />
<div class="flex-grow-1" mat-dialog-content> <div class="flex-grow-1" mat-dialog-content>
<div class="container p-0"> <div class="container p-0">
@ -16,7 +16,7 @@
[locale]="data.locale" [locale]="data.locale"
[unit]="data.baseCurrency" [unit]="data.baseCurrency"
[value]="value" [value]="value"
></gf-value> />
</div> </div>
</div> </div>
@ -33,7 +33,7 @@
[showXAxis]="true" [showXAxis]="true"
[showYAxis]="true" [showYAxis]="true"
[symbol]="data.symbol" [symbol]="data.symbol"
></gf-line-chart> />
<div class="row"> <div class="row">
<div class="col-6 mb-3"> <div class="col-6 mb-3">
@ -222,7 +222,7 @@
[locale]="data.locale" [locale]="data.locale"
[maxItems]="10" [maxItems]="10"
[positions]="sectors" [positions]="sectors"
></gf-portfolio-proportion-chart> />
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<div class="h5" i18n>Countries</div> <div class="h5" i18n>Countries</div>
@ -234,7 +234,7 @@
[locale]="data.locale" [locale]="data.locale"
[maxItems]="10" [maxItems]="10"
[positions]="countries" [positions]="countries"
></gf-portfolio-proportion-chart> />
</div> </div>
</ng-template> </ng-template>
</ng-container> </ng-container>
@ -266,7 +266,7 @@
[sortDisabled]="true" [sortDisabled]="true"
[totalItems]="totalItems" [totalItems]="totalItems"
(export)="onExport()" (export)="onExport()"
></gf-activities-table-lazy> />
<gf-activities-table <gf-activities-table
*ngIf="user?.settings?.isExperimentalFeatures !== true" *ngIf="user?.settings?.isExperimentalFeatures !== true"
[activities]="activities" [activities]="activities"
@ -280,7 +280,7 @@
[showActions]="false" [showActions]="false"
[showNameColumn]="false" [showNameColumn]="false"
(export)="onExport()" (export)="onExport()"
></gf-activities-table> />
</div> </div>
</div> </div>
@ -314,4 +314,4 @@
mat-dialog-actions mat-dialog-actions
[deviceType]="data.deviceType" [deviceType]="data.deviceType"
(closeButtonClicked)="onClose()" (closeButtonClicked)="onClose()"
></gf-dialog-footer> />

View File

@ -18,7 +18,7 @@
[marketState]="position?.marketState" [marketState]="position?.marketState"
[range]="range" [range]="range"
[value]="position?.netPerformancePercentage" [value]="position?.netPerformancePercentage"
></gf-trend-indicator> />
</div> </div>
<div *ngIf="isLoading" class="flex-grow-1"> <div *ngIf="isLoading" class="flex-grow-1">
<ngx-skeleton-loader <ngx-skeleton-loader
@ -28,14 +28,14 @@
height: '1.2rem', height: '1.2rem',
width: '12rem' width: '12rem'
}" }"
></ngx-skeleton-loader> />
<ngx-skeleton-loader <ngx-skeleton-loader
animation="pulse" animation="pulse"
[theme]="{ [theme]="{
height: '1rem', height: '1rem',
width: '8rem' width: '8rem'
}" }"
></ngx-skeleton-loader> />
</div> </div>
<div *ngIf="!isLoading" class="flex-grow-1 text-truncate"> <div *ngIf="!isLoading" class="flex-grow-1 text-truncate">
<div class="h6 m-0 text-truncate">{{ position?.name }}</div> <div class="h6 m-0 text-truncate">{{ position?.name }}</div>
@ -50,13 +50,13 @@
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="position?.netPerformance" [value]="position?.netPerformance"
></gf-value> />
<gf-value <gf-value
[colorizeSign]="true" [colorizeSign]="true"
[isPercent]="true" [isPercent]="true"
[locale]="locale" [locale]="locale"
[value]="position?.netPerformancePercentage" [value]="position?.netPerformancePercentage"
></gf-value> />
</div> </div>
</div> </div>
<div class="align-items-center d-flex"> <div class="align-items-center d-flex">

View File

@ -2,7 +2,7 @@
<div class="row no-gutters"> <div class="row no-gutters">
<div class="col"> <div class="col">
<ng-container *ngIf="positions === undefined"> <ng-container *ngIf="positions === undefined">
<gf-position [isLoading]="true"></gf-position> <gf-position [isLoading]="true" />
</ng-container> </ng-container>
<ng-container *ngIf="positions !== undefined"> <ng-container *ngIf="positions !== undefined">
<ng-container *ngIf="hasPositions"> <ng-container *ngIf="hasPositions">
@ -13,7 +13,7 @@
[locale]="locale" [locale]="locale"
[position]="position" [position]="position"
[range]="range" [range]="range"
></gf-position> />
<gf-position <gf-position
*ngFor="let position of positionsRest" *ngFor="let position of positionsRest"
[baseCurrency]="baseCurrency" [baseCurrency]="baseCurrency"
@ -21,15 +21,13 @@
[locale]="locale" [locale]="locale"
[position]="position" [position]="position"
[range]="range" [range]="range"
></gf-position> />
</ng-container> </ng-container>
<div <div
*ngIf="hasPermissionToCreateOrder && !hasPositions" *ngIf="hasPermissionToCreateOrder && !hasPositions"
class="p-3 text-center" class="p-3 text-center"
> >
<gf-no-transactions-info-indicator <gf-no-transactions-info-indicator [hasBorder]="false" />
[hasBorder]="false"
></gf-no-transactions-info-indicator>
</div> </div>
</ng-container> </ng-container>
</div> </div>

View File

@ -8,7 +8,7 @@
height: '2rem', height: '2rem',
width: '2rem' width: '2rem'
}" }"
></ngx-skeleton-loader> />
</div> </div>
<div <div
*ngIf="!isLoading" *ngIf="!isLoading"
@ -26,14 +26,14 @@
height: '1rem', height: '1rem',
width: '10rem' width: '10rem'
}" }"
></ngx-skeleton-loader> />
<ngx-skeleton-loader <ngx-skeleton-loader
animation="pulse" animation="pulse"
[theme]="{ [theme]="{
height: '1rem', height: '1rem',
width: '15rem' width: '15rem'
}" }"
></ngx-skeleton-loader> />
</div> </div>
<div *ngIf="!isLoading" class="flex-grow-1"> <div *ngIf="!isLoading" class="flex-grow-1">
<div class="h6 my-1">{{ rule?.name }}</div> <div class="h6 my-1">{{ rule?.name }}</div>

View File

@ -7,15 +7,13 @@
class="my-2 text-center" class="my-2 text-center"
> >
<mat-card-content> <mat-card-content>
<gf-no-transactions-info-indicator <gf-no-transactions-info-indicator [hasBorder]="false" />
[hasBorder]="false" </mat-card-content>
></gf-no-transactions-info-indicator
></mat-card-content>
</mat-card> </mat-card>
<gf-rule *ngIf="rules?.length === 0" [isLoading]="true"></gf-rule> <gf-rule *ngIf="rules?.length === 0" [isLoading]="true" />
<ng-container *ngIf="rules !== null && rules !== undefined"> <ng-container *ngIf="rules !== null && rules !== undefined">
<gf-rule *ngFor="let rule of rules" [rule]="rule"></gf-rule> <gf-rule *ngFor="let rule of rules" [rule]="rule" />
</ng-container> </ng-container>
</div> </div>
</div> </div>

View File

@ -7,10 +7,7 @@
<div> <div>
<h5 class="align-items-center d-flex justify-content-center mb-3"> <h5 class="align-items-center d-flex justify-content-center mb-3">
<span>Ghostfolio Premium</span> <span>Ghostfolio Premium</span>
<gf-premium-indicator <gf-premium-indicator class="ml-1" [enableLink]="false" />
class="ml-1"
[enableLink]="false"
></gf-premium-indicator>
</h5> </h5>
<div class="font-weight-normal h5 mb-3 text-center" i18n> <div class="font-weight-normal h5 mb-3 text-center" i18n>
Are you an ambitious investor who needs the full picture? Are you an ambitious investor who needs the full picture?

View File

@ -37,19 +37,23 @@ export class CreateOrUpdateAccessDialog implements OnDestroy {
ngOnInit() { ngOnInit() {
this.accessForm = this.formBuilder.group({ this.accessForm = this.formBuilder.group({
alias: [this.data.access.alias], alias: [this.data.access.alias],
permissions: [this.data.access.permissions[0], Validators.required],
type: [this.data.access.type, Validators.required], type: [this.data.access.type, Validators.required],
userId: [this.data.access.grantee, Validators.required] userId: [this.data.access.grantee, Validators.required]
}); });
this.accessForm.get('type').valueChanges.subscribe((value) => { this.accessForm.get('type').valueChanges.subscribe((accessType) => {
const permissionsControl = this.accessForm.get('permissions');
const userIdControl = this.accessForm.get('userId'); const userIdControl = this.accessForm.get('userId');
if (value === 'PRIVATE') { if (accessType === 'PRIVATE') {
permissionsControl.setValidators(Validators.required);
userIdControl.setValidators(Validators.required); userIdControl.setValidators(Validators.required);
} else { } else {
userIdControl.clearValidators(); userIdControl.clearValidators();
} }
permissionsControl.updateValueAndValidity();
userIdControl.updateValueAndValidity(); userIdControl.updateValueAndValidity();
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
@ -64,7 +68,7 @@ export class CreateOrUpdateAccessDialog implements OnDestroy {
const access: CreateAccessDto = { const access: CreateAccessDto = {
alias: this.accessForm.controls['alias'].value, alias: this.accessForm.controls['alias'].value,
granteeUserId: this.accessForm.controls['userId'].value, granteeUserId: this.accessForm.controls['userId'].value,
type: this.accessForm.controls['type'].value permissions: [this.accessForm.controls['permissions'].value]
}; };
this.dataService this.dataService

View File

@ -30,9 +30,20 @@
@if (accessForm.controls['type'].value === 'PRIVATE') { @if (accessForm.controls['type'].value === 'PRIVATE') {
<div> <div>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label <mat-label i18n>Permission</mat-label>
>Ghostfolio <ng-container i18n>User ID</ng-container></mat-label <mat-select formControlName="permissions">
> <mat-option i18n value="READ_RESTRICTED">Restricted view</mat-option>
@if(data?.user?.settings?.isExperimentalFeatures) {
<mat-option i18n value="READ">View</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div>
<mat-form-field appearance="outline" class="w-100">
<mat-label>
Ghostfolio <ng-container i18n>User ID</ng-container>
</mat-label>
<input <input
formControlName="userId" formControlName="userId"
matInput matInput

View File

@ -1,5 +1,6 @@
import { Access } from '@ghostfolio/common/interfaces'; import { Access, User } from '@ghostfolio/common/interfaces';
export interface CreateOrUpdateAccessDialogParams { export interface CreateOrUpdateAccessDialogParams {
access: Access; access: Access;
user: User;
} }

View File

@ -7,7 +7,6 @@ import {
} from '@angular/core'; } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { Access, User } from '@ghostfolio/common/interfaces'; import { Access, User } from '@ghostfolio/common/interfaces';
@ -105,8 +104,10 @@ export class UserAccountAccessComponent implements OnDestroy, OnInit {
data: { data: {
access: { access: {
alias: '', alias: '',
permissions: ['READ_RESTRICTED'],
type: 'PRIVATE' type: 'PRIVATE'
} },
user: this.user
}, },
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
width: this.deviceType === 'mobile' ? '100vw' : '50rem' width: this.deviceType === 'mobile' ? '100vw' : '50rem'

View File

@ -6,13 +6,13 @@
<gf-premium-indicator <gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'" *ngIf="user?.subscription?.type === 'Basic'"
class="ml-1" class="ml-1"
></gf-premium-indicator> />
</h1> </h1>
<gf-access-table <gf-access-table
[accesses]="accesses" [accesses]="accesses"
[showActions]="hasPermissionToDeleteAccess" [showActions]="hasPermissionToDeleteAccess"
(accessDeleted)="onDeleteAccess($event)" (accessDeleted)="onDeleteAccess($event)"
></gf-access-table> />
<div *ngIf="hasPermissionToCreateAccess" class="fab-container"> <div *ngIf="hasPermissionToCreateAccess" class="fab-container">
<a <a
class="align-items-center d-flex justify-content-center" class="align-items-center d-flex justify-content-center"

View File

@ -5,7 +5,7 @@
<gf-membership-card <gf-membership-card
[expiresAt]="user?.subscription?.expiresAt | date: defaultDateFormat" [expiresAt]="user?.subscription?.expiresAt | date: defaultDateFormat"
[name]="user?.subscription?.type" [name]="user?.subscription?.type"
></gf-membership-card> />
<div <div
*ngIf="user?.subscription?.type === 'Basic'" *ngIf="user?.subscription?.type === 'Basic'"
class="d-flex flex-column mt-5" class="d-flex flex-column mt-5"
@ -15,10 +15,10 @@
> >
<button color="primary" mat-flat-button (click)="onCheckout()"> <button color="primary" mat-flat-button (click)="onCheckout()">
<ng-container *ngIf="user.subscription.offer === 'default'" i18n <ng-container *ngIf="user.subscription.offer === 'default'" i18n
>Upgrade</ng-container >Upgrade Plan</ng-container
> >
<ng-container *ngIf="user.subscription.offer === 'renewal'" i18n <ng-container *ngIf="user.subscription.offer === 'renewal'" i18n
>Renew</ng-container >Renew Plan</ng-container
> >
</button> </button>
<div *ngIf="price" class="mt-1 text-center"> <div *ngIf="price" class="mt-1 text-center">
@ -43,8 +43,8 @@
<gf-premium-indicator <gf-premium-indicator
class="d-inline-block ml-1" class="d-inline-block ml-1"
[enableLink]="false" [enableLink]="false"
></gf-premium-indicator />
></a> </a>
<a <a
*ngIf="hasPermissionToUpdateUserSettings" *ngIf="hasPermissionToUpdateUserSettings"
class="mx-1" class="mx-1"

View File

@ -5,6 +5,6 @@
[theme]="{ [theme]="{
width: '100%' width: '100%'
}" }"
></ngx-skeleton-loader> />
<div class="align-items-center d-flex h-100 w-100" id="svgMap"></div> <div class="align-items-center d-flex h-100 w-100" id="svgMap"></div>

View File

@ -15,7 +15,7 @@
(accountDeleted)="onDeleteAccount($event)" (accountDeleted)="onDeleteAccount($event)"
(accountToUpdate)="onUpdateAccount($event)" (accountToUpdate)="onUpdateAccount($event)"
(transferBalance)="onTransferBalance()" (transferBalance)="onTransferBalance()"
></gf-accounts-table> />
</div> </div>
</div> </div>
</div> </div>

View File

@ -61,7 +61,7 @@
class="mr-1" class="mr-1"
[tooltip]="platformEntry.name" [tooltip]="platformEntry.name"
[url]="platformEntry.url" [url]="platformEntry.url"
></gf-symbol-icon> />
<span>{{ platformEntry.name }}</span> <span>{{ platformEntry.name }}</span>
</span> </span>
</mat-option> </mat-option>

View File

@ -17,8 +17,7 @@
class="mr-1" class="mr-1"
[tooltip]="account.Platform?.name" [tooltip]="account.Platform?.name"
[url]="account.Platform?.url" [url]="account.Platform?.url"
></gf-symbol-icon /><span>{{ account.name }}</span>
><span>{{ account.name }}</span>
</div> </div>
</mat-option> </mat-option>
</mat-select> </mat-select>
@ -35,8 +34,7 @@
class="mr-1" class="mr-1"
[tooltip]="account.Platform?.name" [tooltip]="account.Platform?.name"
[url]="account.Platform?.url" [url]="account.Platform?.url"
></gf-symbol-icon /><span>{{ account.name }}</span>
><span>{{ account.name }}</span>
</div> </div>
</mat-option> </mat-option>
</mat-select> </mat-select>

View File

@ -20,8 +20,8 @@
<gf-premium-indicator <gf-premium-indicator
class="d-inline-block ml-1" class="d-inline-block ml-1"
[enableLink]="false" [enableLink]="false"
></gf-premium-indicator />
></span> </span>
annual plan for ambitious investors who need the full picture of annual plan for ambitious investors who need the full picture of
their financial assets. their financial assets.
</p> </p>

View File

@ -21,8 +21,8 @@
<gf-premium-indicator <gf-premium-indicator
class="d-inline-block ml-1" class="d-inline-block ml-1"
[enableLink]="false" [enableLink]="false"
></gf-premium-indicator />
></span> </span>
annual plan with our exclusive Black Week deal. Elevate your annual plan with our exclusive Black Week deal. Elevate your
financial strategy with the power of Ghostfolio designed to give you financial strategy with the power of Ghostfolio designed to give you
the full picture of your assets. the full picture of your assets.

View File

@ -50,11 +50,8 @@
You can sign up via the “<a [routerLink]="routerLinkRegister" You can sign up via the “<a [routerLink]="routerLinkRegister"
>Get Started</a >Get Started</a
>” button at the top of the page. You have multiple options to join >” button at the top of the page. You have multiple options to join
Ghostfolio: Create an account with a security token, using Ghostfolio: Create an account with a security token or
<a href="../en/blog/2022/07/ghostfolio-meets-internet-identity" <i>Google Sign</i>. We will guide you to set up your portfolio.
>Internet Identity</a
>
or <i>Google Sign</i>. We will guide you to set up your portfolio.
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
<mat-card appearance="outlined" class="mb-3"> <mat-card appearance="outlined" class="mb-3">
@ -75,11 +72,9 @@
></mat-card-header ></mat-card-header
> >
<mat-card-content> <mat-card-content>
Yes, the authentication systems (via security token or Yes, the authentication system via security token enables you to sign
<a href="../en/blog/2022/07/ghostfolio-meets-internet-identity" in securely and anonymously to Ghostfolio. There is no need for an
>Internet Identity</a e-mail address, phone number, or a username.
>) enable you to sign in securely and anonymously to Ghostfolio. There
is no need for an e-mail address, phone number, or a username.
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
<mat-card appearance="outlined" class="mb-3"> <mat-card appearance="outlined" class="mb-3">
@ -176,6 +171,19 @@
your university e-mail address.</mat-card-content your university e-mail address.</mat-card-content
> >
</mat-card> </mat-card>
<mat-card appearance="outlined" class="mb-3">
<mat-card-header>
<mat-card-title
>Does the Ghostfolio Premium subscription renew
automatically?</mat-card-title
>
</mat-card-header>
<mat-card-content
>No, <a [routerLink]="routerLinkPricing">Ghostfolio Premium</a> does
not include auto-renewal. Upon expiration, you can choose whether to
start a new subscription.</mat-card-content
>
</mat-card>
<mat-card appearance="outlined" class="mb-3"> <mat-card appearance="outlined" class="mb-3">
<mat-card-header> <mat-card-header>
<mat-card-title>Which devices are supported?</mat-card-title> <mat-card-title>Which devices are supported?</mat-card-title>

View File

@ -142,7 +142,7 @@
<gf-premium-indicator <gf-premium-indicator
*ngIf="hasPermissionForSubscription" *ngIf="hasPermissionForSubscription"
class="ml-1" class="ml-1"
></gf-premium-indicator> />
</h4> </h4>
<p class="m-0"> <p class="m-0">
Check the rate of return of your portfolio for Check the rate of return of your portfolio for
@ -162,7 +162,7 @@
<gf-premium-indicator <gf-premium-indicator
*ngIf="hasPermissionForSubscription" *ngIf="hasPermissionForSubscription"
class="ml-1" class="ml-1"
></gf-premium-indicator> />
</h4> </h4>
<p class="m-0"> <p class="m-0">
Check the allocations of your portfolio by account, asset Check the allocations of your portfolio by account, asset
@ -207,7 +207,7 @@
<div class="flex-grow-1"> <div class="flex-grow-1">
<h4 class="align-items-center d-flex"> <h4 class="align-items-center d-flex">
<span i18n>Market Mood</span> <span i18n>Market Mood</span>
<gf-premium-indicator class="ml-1"></gf-premium-indicator> <gf-premium-indicator class="ml-1" />
</h4> </h4>
<p class="m-0"> <p class="m-0">
Check the current market mood (<a Check the current market mood (<a
@ -228,7 +228,7 @@
<gf-premium-indicator <gf-premium-indicator
*ngIf="hasPermissionForSubscription" *ngIf="hasPermissionForSubscription"
class="ml-1" class="ml-1"
></gf-premium-indicator> />
</h4> </h4>
<p class="m-0"> <p class="m-0">
Identify potential risks in your portfolio with Ghostfolio Identify potential risks in your portfolio with Ghostfolio

View File

@ -328,11 +328,7 @@
<gf-carousel [aria-label]="'Testimonials'"> <gf-carousel [aria-label]="'Testimonials'">
<div *ngFor="let testimonial of testimonials" gf-carousel-item> <div *ngFor="let testimonial of testimonials" gf-carousel-item>
<div class="d-flex px-4"> <div class="d-flex px-4">
<gf-logo <gf-logo class="mr-3 mt-2 pt-1" size="medium" [showLabel]="false" />
class="mr-3 mt-2 pt-1"
size="medium"
[showLabel]="false"
></gf-logo>
<div> <div>
<div>{{ testimonial.quote }}</div> <div>{{ testimonial.quote }}</div>
<div class="mt-2 text-muted"> <div class="mt-2 text-muted">
@ -361,10 +357,7 @@
</h2> </h2>
</div> </div>
<div class="col-md-8 customer-map-container offset-md-2"> <div class="col-md-8 customer-map-container offset-md-2">
<gf-world-map-chart <gf-world-map-chart format="👻" [countries]="countriesOfSubscribersMap" />
format="👻"
[countries]="countriesOfSubscribersMap"
></gf-world-map-chart>
</div> </div>
</div> </div>
@ -450,7 +443,7 @@
<div <div
class="align-items-center d-flex flex-column justify-content-center w-100" class="align-items-center d-flex flex-column justify-content-center w-100"
> >
<gf-logo size="medium"></gf-logo> <gf-logo size="medium" />
<div>Wealth Management Software</div> <div>Wealth Management Software</div>
</div> </div>
</div> </div>

View File

@ -1,7 +1,7 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<gf-home-market></gf-home-market> <gf-home-market />
</div> </div>
</div> </div>
</div> </div>

View File

@ -15,7 +15,7 @@ import { ImpersonationStorageService } from '@ghostfolio/client/services/imperso
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config'; import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
import { downloadAsFile } from '@ghostfolio/common/helper'; import { downloadAsFile } from '@ghostfolio/common/helper';
import { Filter, User } from '@ghostfolio/common/interfaces'; import { User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { DataSource, Order as OrderModel } from '@prisma/client'; import { DataSource, Order as OrderModel } from '@prisma/client';
import { format, parseISO } from 'date-fns'; import { format, parseISO } from 'date-fns';
@ -199,8 +199,14 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
} }
public onExport(activityIds?: string[]) { public onExport(activityIds?: string[]) {
let fetchExportParams: any = { activityIds };
if (!activityIds) {
fetchExportParams = { filters: this.userService.getFilters() };
}
this.dataService this.dataService
.fetchExport(activityIds) .fetchExport(fetchExportParams)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((data) => { .subscribe((data) => {
for (const activity of data.activities) { for (const activity of data.activities) {
@ -220,7 +226,7 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
public onExportDrafts(activityIds?: string[]) { public onExportDrafts(activityIds?: string[]) {
this.dataService this.dataService
.fetchExport(activityIds) .fetchExport({ activityIds })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((data) => { .subscribe((data) => {
downloadAsFile({ downloadAsFile({

View File

@ -20,13 +20,13 @@
(activityToClone)="onCloneActivity($event)" (activityToClone)="onCloneActivity($event)"
(activityToUpdate)="onUpdateActivity($event)" (activityToUpdate)="onUpdateActivity($event)"
(deleteAllActivities)="onDeleteAllActivities()" (deleteAllActivities)="onDeleteAllActivities()"
(export)="onExport($event)" (export)="onExport()"
(exportDrafts)="onExportDrafts($event)" (exportDrafts)="onExportDrafts($event)"
(import)="onImport()" (import)="onImport()"
(importDividends)="onImportDividends()" (importDividends)="onImportDividends()"
(pageChanged)="onChangePage($event)" (pageChanged)="onChangePage($event)"
(sortChanged)="onSortChanged($event)" (sortChanged)="onSortChanged($event)"
></gf-activities-table-lazy> />
<gf-activities-table <gf-activities-table
*ngIf="user?.settings?.isExperimentalFeatures !== true" *ngIf="user?.settings?.isExperimentalFeatures !== true"
[activities]="activities" [activities]="activities"
@ -44,7 +44,7 @@
(exportDrafts)="onExportDrafts($event)" (exportDrafts)="onExportDrafts($event)"
(import)="onImport()" (import)="onImport()"
(importDividends)="onImportDividends()" (importDividends)="onImportDividends()"
></gf-activities-table> />
</div> </div>
</div> </div>

View File

@ -82,8 +82,7 @@
class="mr-1" class="mr-1"
[tooltip]="account.Platform?.name" [tooltip]="account.Platform?.name"
[url]="account.Platform?.url" [url]="account.Platform?.url"
></gf-symbol-icon /><span>{{ account.name }}</span>
><span>{{ account.name }}</span>
</div> </div>
</mat-option> </mat-option>
</mat-select> </mat-select>
@ -357,7 +356,7 @@
[locale]="data.user?.settings?.locale" [locale]="data.user?.settings?.locale"
[unit]="activityForm.controls['currency']?.value ?? data.user?.settings?.baseCurrency" [unit]="activityForm.controls['currency']?.value ?? data.user?.settings?.baseCurrency"
[value]="total" [value]="total"
></gf-value> />
<div> <div>
<button i18n mat-button type="button" (click)="onCancel()">Cancel</button> <button i18n mat-button type="button" (click)="onCancel()">Cancel</button>
<button <button

View File

@ -3,7 +3,7 @@
[deviceType]="data.deviceType" [deviceType]="data.deviceType"
[title]="dialogTitle" [title]="dialogTitle"
(closeButtonClicked)="onCancel()" (closeButtonClicked)="onCancel()"
></gf-dialog-header> />
<div class="flex-grow-1" mat-dialog-content> <div class="flex-grow-1" mat-dialog-content>
<mat-stepper <mat-stepper
@ -136,7 +136,7 @@
[sortDisabled]="true" [sortDisabled]="true"
[totalItems]="totalItems" [totalItems]="totalItems"
(selectedActivities)="updateSelection($event)" (selectedActivities)="updateSelection($event)"
></gf-activities-table-lazy> />
<gf-activities-table <gf-activities-table
*ngIf="importStep === 1 && data?.user?.settings?.isExperimentalFeatures !== true" *ngIf="importStep === 1 && data?.user?.settings?.isExperimentalFeatures !== true"
[activities]="activities" [activities]="activities"
@ -153,7 +153,7 @@
[showFooter]="false" [showFooter]="false"
[showSymbolColumn]="false" [showSymbolColumn]="false"
(selectedActivities)="updateSelection($event)" (selectedActivities)="updateSelection($event)"
></gf-activities-table> />
<div class="d-flex justify-content-end mt-3"> <div class="d-flex justify-content-end mt-3">
<button mat-button (click)="onReset(stepper)"> <button mat-button (click)="onReset(stepper)">
<ng-container i18n>Back</ng-container> <ng-container i18n>Back</ng-container>
@ -214,4 +214,4 @@
mat-dialog-actions mat-dialog-actions
[deviceType]="data.deviceType" [deviceType]="data.deviceType"
(closeButtonClicked)="onCancel()" (closeButtonClicked)="onCancel()"
></gf-dialog-footer> />

View File

@ -148,9 +148,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
this.initialize(); this.initialize();
return this.dataService.fetchPortfolioDetails({ return this.fetchPortfolioDetails();
filters: this.activeFilters
});
}), }),
takeUntil(this.unsubscribeSubject) takeUntil(this.unsubscribeSubject)
) )
@ -159,7 +157,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
this.portfolioDetails = portfolioDetails; this.portfolioDetails = portfolioDetails;
this.initializeAnalysisData(); this.initializeAllocationsData();
this.isLoading = false; this.isLoading = false;
@ -210,6 +208,26 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
? `{0}%` ? `{0}%`
: `{0} ${this.user?.settings?.baseCurrency}`; : `{0} ${this.user?.settings?.baseCurrency}`;
if (this.user?.settings?.isExperimentalFeatures === true) {
this.isLoading = true;
this.initialize();
this.fetchPortfolioDetails()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((portfolioDetails) => {
this.initialize();
this.portfolioDetails = portfolioDetails;
this.initializeAllocationsData();
this.isLoading = false;
this.changeDetectorRef.markForCheck();
});
}
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
} }
}); });
@ -217,7 +235,52 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
this.initialize(); this.initialize();
} }
public initialize() { public onAccountChartClicked({ symbol }: UniqueAsset) {
if (symbol && symbol !== UNKNOWN_KEY) {
this.router.navigate([], {
queryParams: { accountId: symbol, accountDetailDialog: true }
});
}
}
public onSymbolChartClicked({ dataSource, symbol }: UniqueAsset) {
if (dataSource && symbol) {
this.router.navigate([], {
queryParams: { dataSource, symbol, positionDetailDialog: true }
});
}
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private extractEtfProvider({
assetSubClass,
name
}: {
assetSubClass: PortfolioPosition['assetSubClass'];
name: string;
}) {
if (assetSubClass === 'ETF') {
const [firstWord] = name.split(' ');
return firstWord;
}
return UNKNOWN_KEY;
}
private fetchPortfolioDetails() {
return this.dataService.fetchPortfolioDetails({
filters:
this.activeFilters.length > 0
? this.activeFilters
: this.userService.getFilters()
});
}
private initialize() {
this.accounts = {}; this.accounts = {};
this.continents = { this.continents = {
[UNKNOWN_KEY]: { [UNKNOWN_KEY]: {
@ -310,7 +373,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
}; };
} }
public initializeAnalysisData() { private initializeAllocationsData() {
for (const [ for (const [
id, id,
{ name, valueInBaseCurrency, valueInPercentage } { name, valueInBaseCurrency, valueInPercentage }
@ -540,27 +603,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
this.markets[UNKNOWN_KEY].value / marketsTotal; this.markets[UNKNOWN_KEY].value / marketsTotal;
} }
public onAccountChartClicked({ symbol }: UniqueAsset) {
if (symbol && symbol !== UNKNOWN_KEY) {
this.router.navigate([], {
queryParams: { accountId: symbol, accountDetailDialog: true }
});
}
}
public onSymbolChartClicked({ dataSource, symbol }: UniqueAsset) {
if (dataSource && symbol) {
this.router.navigate([], {
queryParams: { dataSource, symbol, positionDetailDialog: true }
});
}
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private openAccountDetailDialog(aAccountId: string) { private openAccountDetailDialog(aAccountId: string) {
const dialogRef = this.dialog.open(AccountDetailDialog, { const dialogRef = this.dialog.open(AccountDetailDialog, {
autoFocus: false, autoFocus: false,
@ -621,19 +663,4 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
}); });
}); });
} }
private extractEtfProvider({
assetSubClass,
name
}: {
assetSubClass: PortfolioPosition['assetSubClass'];
name: string;
}) {
if (assetSubClass === 'ETF') {
const [firstWord] = name.split(' ');
return firstWord;
}
return UNKNOWN_KEY;
}
} }

View File

@ -2,12 +2,14 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Allocations</h1> <h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Allocations</h1>
@if (!user?.settings?.isExperimentalFeatures) {
<gf-activities-filter <gf-activities-filter
[allFilters]="allFilters" [allFilters]="allFilters"
[isLoading]="isLoading" [isLoading]="isLoading"
[placeholder]="placeholder" [placeholder]="placeholder"
(valueChanged)="filters$.next($event)" (valueChanged)="filters$.next($event)"
></gf-activities-filter> />
}
</div> </div>
</div> </div>
<div class="row"> <div class="row">
@ -22,7 +24,7 @@
size="medium" size="medium"
[isPercent]="true" [isPercent]="true"
[value]="isLoading ? undefined : portfolioDetails?.filteredValueInPercentage" [value]="isLoading ? undefined : portfolioDetails?.filteredValueInPercentage"
></gf-value> />
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<mat-progress-bar <mat-progress-bar
@ -50,7 +52,7 @@
[keys]="['id']" [keys]="['id']"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[positions]="platforms" [positions]="platforms"
></gf-portfolio-proportion-chart> />
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
</div> </div>
@ -62,8 +64,8 @@
<gf-premium-indicator <gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'" *ngIf="user?.subscription?.type === 'Basic'"
class="ml-1" class="ml-1"
></gf-premium-indicator />
></mat-card-title> </mat-card-title>
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<gf-portfolio-proportion-chart <gf-portfolio-proportion-chart
@ -73,7 +75,7 @@
[keys]="['currency']" [keys]="['currency']"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[positions]="positions" [positions]="positions"
></gf-portfolio-proportion-chart> />
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
</div> </div>
@ -85,8 +87,8 @@
><gf-premium-indicator ><gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'" *ngIf="user?.subscription?.type === 'Basic'"
class="ml-1" class="ml-1"
></gf-premium-indicator />
></mat-card-title> </mat-card-title>
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<gf-portfolio-proportion-chart <gf-portfolio-proportion-chart
@ -96,7 +98,7 @@
[keys]="['assetClassLabel', 'assetSubClassLabel']" [keys]="['assetClassLabel', 'assetSubClassLabel']"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[positions]="positions" [positions]="positions"
></gf-portfolio-proportion-chart> />
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
</div> </div>
@ -119,7 +121,7 @@
[positions]="symbols" [positions]="symbols"
[showLabels]="deviceType !== 'mobile'" [showLabels]="deviceType !== 'mobile'"
(proportionChartClicked)="onSymbolChartClicked($event)" (proportionChartClicked)="onSymbolChartClicked($event)"
></gf-portfolio-proportion-chart> />
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
</div> </div>
@ -131,8 +133,8 @@
><gf-premium-indicator ><gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'" *ngIf="user?.subscription?.type === 'Basic'"
class="ml-1" class="ml-1"
></gf-premium-indicator />
></mat-card-title> </mat-card-title>
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<gf-portfolio-proportion-chart <gf-portfolio-proportion-chart
@ -143,7 +145,7 @@
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[maxItems]="10" [maxItems]="10"
[positions]="sectors" [positions]="sectors"
></gf-portfolio-proportion-chart> />
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
</div> </div>
@ -155,8 +157,8 @@
><gf-premium-indicator ><gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'" *ngIf="user?.subscription?.type === 'Basic'"
class="ml-1" class="ml-1"
></gf-premium-indicator />
></mat-card-title> </mat-card-title>
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<gf-portfolio-proportion-chart <gf-portfolio-proportion-chart
@ -166,7 +168,7 @@
[keys]="['name']" [keys]="['name']"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[positions]="continents" [positions]="continents"
></gf-portfolio-proportion-chart> />
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
</div> </div>
@ -178,8 +180,8 @@
><gf-premium-indicator ><gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'" *ngIf="user?.subscription?.type === 'Basic'"
class="ml-1" class="ml-1"
></gf-premium-indicator />
></mat-card-title> </mat-card-title>
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<gf-portfolio-proportion-chart <gf-portfolio-proportion-chart
@ -188,7 +190,7 @@
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView" [isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[positions]="marketsAdvanced" [positions]="marketsAdvanced"
></gf-portfolio-proportion-chart> />
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
</div> </div>
@ -202,8 +204,8 @@
><gf-premium-indicator ><gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'" *ngIf="user?.subscription?.type === 'Basic'"
class="ml-1" class="ml-1"
></gf-premium-indicator />
></mat-card-title> </mat-card-title>
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<div class="world-map-chart-container"> <div class="world-map-chart-container">
@ -212,7 +214,7 @@
[format]="worldMapChartFormat" [format]="worldMapChartFormat"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView" [isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
></gf-world-map-chart> />
</div> </div>
<div class="row"> <div class="row">
<div class="col-xs-12 col-md my-2"> <div class="col-xs-12 col-md my-2">
@ -275,7 +277,7 @@
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[positions]="accounts" [positions]="accounts"
(proportionChartClicked)="onAccountChartClicked($event)" (proportionChartClicked)="onAccountChartClicked($event)"
></gf-portfolio-proportion-chart> />
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
</div> </div>
@ -287,8 +289,8 @@
><gf-premium-indicator ><gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'" *ngIf="user?.subscription?.type === 'Basic'"
class="ml-1" class="ml-1"
></gf-premium-indicator />
></mat-card-title> </mat-card-title>
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<gf-portfolio-proportion-chart <gf-portfolio-proportion-chart
@ -298,7 +300,7 @@
[keys]="['etfProvider']" [keys]="['etfProvider']"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[positions]="positions" [positions]="positions"
></gf-portfolio-proportion-chart> />
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
</div> </div>
@ -310,8 +312,8 @@
><gf-premium-indicator ><gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'" *ngIf="user?.subscription?.type === 'Basic'"
class="ml-1" class="ml-1"
></gf-premium-indicator />
></mat-card-title> </mat-card-title>
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<gf-portfolio-proportion-chart <gf-portfolio-proportion-chart
@ -322,7 +324,7 @@
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[maxItems]="10" [maxItems]="10"
[positions]="countries" [positions]="countries"
></gf-portfolio-proportion-chart> />
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
</div> </div>

View File

@ -379,7 +379,9 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ positions }) => { .subscribe(({ positions }) => {
const positionsSorted = sortBy( const positionsSorted = sortBy(
positions, positions.filter(({ netPerformancePercentage }) => {
return isNumber(netPerformancePercentage);
}),
'netPerformancePercentage' 'netPerformancePercentage'
).reverse(); ).reverse();

View File

@ -7,14 +7,14 @@
[isLoading]="isLoadingBenchmarkComparator || isLoadingInvestmentChart" [isLoading]="isLoadingBenchmarkComparator || isLoadingInvestmentChart"
[options]="dateRangeOptions" [options]="dateRangeOptions"
(change)="onChangeDateRange($event.value)" (change)="onChangeDateRange($event.value)"
></gf-toggle> />
</div> </div>
<gf-activities-filter <gf-activities-filter
[allFilters]="allFilters" [allFilters]="allFilters"
[isLoading]="isLoadingBenchmarkComparator || isLoadingInvestmentChart" [isLoading]="isLoadingBenchmarkComparator || isLoadingInvestmentChart"
[placeholder]="placeholder" [placeholder]="placeholder"
(valueChanged)="filters$.next($event)" (valueChanged)="filters$.next($event)"
></gf-activities-filter> />
} }
<div class="mb-5 row"> <div class="mb-5 row">
<div class="col-lg"> <div class="col-lg">
@ -30,7 +30,7 @@
[performanceDataItems]="performanceDataItemsInPercentage" [performanceDataItems]="performanceDataItemsInPercentage"
[user]="user" [user]="user"
(benchmarkChanged)="onChangeBenchmark($event)" (benchmarkChanged)="onChangeBenchmark($event)"
></gf-benchmark-comparator> />
</div> </div>
</div> </div>
@ -51,7 +51,7 @@
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency" [unit]="user?.settings?.baseCurrency"
[value]="isLoadingInvestmentChart ? undefined : performance?.currentNetPerformance" [value]="isLoadingInvestmentChart ? undefined : performance?.currentNetPerformance"
></gf-value> />
</div> </div>
</div> </div>
<div class="d-flex mb-3 ml-3 py-1"> <div class="d-flex mb-3 ml-3 py-1">
@ -66,7 +66,7 @@
[isPercent]="true" [isPercent]="true"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[value]="isLoadingInvestmentChart ? undefined : performance?.currentNetPerformancePercent" [value]="isLoadingInvestmentChart ? undefined : performance?.currentNetPerformancePercent"
></gf-value> />
</div> </div>
</div> </div>
<div class="d-flex py-1"> <div class="d-flex py-1">
@ -81,7 +81,7 @@
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency" [unit]="user?.settings?.baseCurrency"
[value]="isLoadingInvestmentChart ? undefined : (performance?.currentNetPerformanceWithCurrencyEffect === null ? null : performance?.currentNetPerformanceWithCurrencyEffect - performance?.currentNetPerformance)" [value]="isLoadingInvestmentChart ? undefined : (performance?.currentNetPerformanceWithCurrencyEffect === null ? null : performance?.currentNetPerformanceWithCurrencyEffect - performance?.currentNetPerformance)"
></gf-value> />
</div> </div>
</div> </div>
<div class="d-flex ml-3 py-1"> <div class="d-flex ml-3 py-1">
@ -96,7 +96,7 @@
[isPercent]="true" [isPercent]="true"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[value]="isLoadingInvestmentChart ? undefined : performance?.currentNetPerformancePercentWithCurrencyEffect - performance?.currentNetPerformancePercent" [value]="isLoadingInvestmentChart ? undefined : performance?.currentNetPerformancePercentWithCurrencyEffect - performance?.currentNetPerformancePercent"
></gf-value> />
</div> </div>
</div> </div>
<div><hr /></div> <div><hr /></div>
@ -112,7 +112,7 @@
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency" [unit]="user?.settings?.baseCurrency"
[value]="isLoadingInvestmentChart ? undefined : performance?.currentNetPerformanceWithCurrencyEffect" [value]="isLoadingInvestmentChart ? undefined : performance?.currentNetPerformanceWithCurrencyEffect"
></gf-value> />
</div> </div>
</div> </div>
<div class="d-flex ml-3 py-1"> <div class="d-flex ml-3 py-1">
@ -127,7 +127,7 @@
[isPercent]="true" [isPercent]="true"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[value]="isLoadingInvestmentChart ? undefined : performance?.currentNetPerformancePercentWithCurrencyEffect" [value]="isLoadingInvestmentChart ? undefined : performance?.currentNetPerformancePercentWithCurrencyEffect"
></gf-value> />
</div> </div>
</div> </div>
</mat-card-content> </mat-card-content>
@ -167,7 +167,7 @@
[isPercent]="true" [isPercent]="true"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[value]="position.netPerformancePercentage" [value]="position.netPerformancePercentage"
></gf-value> />
</div> </div>
</a> </a>
</li> </li>
@ -180,7 +180,7 @@
height: '1.5rem', height: '1.5rem',
width: '100%' width: '100%'
}" }"
></ngx-skeleton-loader> />
</div> </div>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
@ -215,7 +215,7 @@
[isPercent]="true" [isPercent]="true"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[value]="position.netPerformancePercentage" [value]="position.netPerformancePercentage"
></gf-value> />
</div> </div>
</a> </a>
</li> </li>
@ -228,7 +228,7 @@
height: '1.5rem', height: '1.5rem',
width: '100%' width: '100%'
}" }"
></ngx-skeleton-loader> />
</div> </div>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
@ -245,7 +245,7 @@
<gf-premium-indicator <gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'" *ngIf="user?.subscription?.type === 'Basic'"
class="ml-1" class="ml-1"
></gf-premium-indicator> />
</div> </div>
</div> </div>
<div class="chart-container"> <div class="chart-container">
@ -260,7 +260,7 @@
[isLoading]="isLoadingInvestmentChart" [isLoading]="isLoadingInvestmentChart"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[range]="user?.settings?.dateRange" [range]="user?.settings?.dateRange"
></gf-investment-chart> />
</div> </div>
</div> </div>
</div> </div>
@ -275,7 +275,7 @@
<gf-premium-indicator <gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'" *ngIf="user?.subscription?.type === 'Basic'"
class="ml-1" class="ml-1"
></gf-premium-indicator> />
</div> </div>
<gf-toggle <gf-toggle
class="d-none d-lg-block" class="d-none d-lg-block"
@ -283,7 +283,7 @@
[isLoading]="false" [isLoading]="false"
[options]="modeOptions" [options]="modeOptions"
(change)="onChangeGroupBy($event.value)" (change)="onChangeGroupBy($event.value)"
></gf-toggle> />
</div> </div>
<div *ngIf="streaks" class="row"> <div *ngIf="streaks" class="row">
<div class="col-md-6 col-xs-12 my-2"> <div class="col-md-6 col-xs-12 my-2">
@ -317,7 +317,7 @@
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[range]="user?.settings?.dateRange" [range]="user?.settings?.dateRange"
[savingsRate]="savingsRate" [savingsRate]="savingsRate"
></gf-investment-chart> />
</div> </div>
</div> </div>
</div> </div>
@ -332,7 +332,7 @@
<gf-premium-indicator <gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'" *ngIf="user?.subscription?.type === 'Basic'"
class="ml-1" class="ml-1"
></gf-premium-indicator> />
</div> </div>
<gf-toggle <gf-toggle
class="d-none d-lg-block" class="d-none d-lg-block"
@ -340,7 +340,7 @@
[isLoading]="false" [isLoading]="false"
[options]="modeOptions" [options]="modeOptions"
(change)="onChangeGroupBy($event.value)" (change)="onChangeGroupBy($event.value)"
></gf-toggle> />
</div> </div>
<div class="chart-container"> <div class="chart-container">
<gf-investment-chart <gf-investment-chart
@ -353,7 +353,7 @@
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView" [isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[range]="user?.settings?.dateRange" [range]="user?.settings?.dateRange"
></gf-investment-chart> />
</div> </div>
</div> </div>
</div> </div>

View File

@ -8,7 +8,7 @@
><gf-premium-indicator ><gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'" *ngIf="user?.subscription?.type === 'Basic'"
class="ml-1" class="ml-1"
></gf-premium-indicator> />
</h4> </h4>
<gf-fire-calculator <gf-fire-calculator
[annualInterestRate]="user?.settings?.annualInterestRate" [annualInterestRate]="user?.settings?.annualInterestRate"
@ -25,7 +25,7 @@
(projectedTotalAmountChanged)="onProjectedTotalAmountChange($event)" (projectedTotalAmountChanged)="onProjectedTotalAmountChange($event)"
(retirementDateChanged)="onRetirementDateChange($event)" (retirementDateChanged)="onRetirementDateChange($event)"
(savingsRateChanged)="onSavingsRateChange($event)" (savingsRateChanged)="onSavingsRateChange($event)"
></gf-fire-calculator> />
</div> </div>
</div> </div>
</div> </div>
@ -35,7 +35,7 @@
><gf-premium-indicator ><gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'" *ngIf="user?.subscription?.type === 'Basic'"
class="ml-1" class="ml-1"
></gf-premium-indicator> />
</h4> </h4>
<div *ngIf="isLoading"> <div *ngIf="isLoading">
<ngx-skeleton-loader <ngx-skeleton-loader
@ -45,14 +45,14 @@
height: '1rem', height: '1rem',
width: '100%' width: '100%'
}" }"
></ngx-skeleton-loader> />
<ngx-skeleton-loader <ngx-skeleton-loader
animation="pulse" animation="pulse"
[theme]="{ [theme]="{
height: '1rem', height: '1rem',
width: '10rem' width: '10rem'
}" }"
></ngx-skeleton-loader> />
</div> </div>
<div *ngIf="!isLoading" i18n> <div *ngIf="!isLoading" i18n>
If you retire today, you would be able to withdraw If you retire today, you would be able to withdraw
@ -63,7 +63,7 @@
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency" [unit]="user?.settings?.baseCurrency"
[value]="withdrawalRatePerYear?.toNumber()" [value]="withdrawalRatePerYear?.toNumber()"
></gf-value> />
per year</span per year</span
> >
or or
@ -74,7 +74,7 @@
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency" [unit]="user?.settings?.baseCurrency"
[value]="withdrawalRatePerMonth?.toNumber()" [value]="withdrawalRatePerMonth?.toNumber()"
></gf-value> />
per month</span per month</span
>, based on your total assets of >, based on your total assets of
<span class="font-weight-bold" <span class="font-weight-bold"
@ -84,8 +84,8 @@
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency" [unit]="user?.settings?.baseCurrency"
[value]="fireWealth?.toNumber()" [value]="fireWealth?.toNumber()"
></gf-value />
></span> </span>
and a withdrawal rate of 4%. and a withdrawal rate of 4%.
</div> </div>
</div> </div>
@ -112,12 +112,12 @@
><gf-premium-indicator ><gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'" *ngIf="user?.subscription?.type === 'Basic'"
class="ml-1" class="ml-1"
></gf-premium-indicator> />
</h4> </h4>
<gf-rules <gf-rules
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder" [hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
[rules]="emergencyFundRules" [rules]="emergencyFundRules"
></gf-rules> />
</div> </div>
<div class="mb-4"> <div class="mb-4">
<h4 class="align-items-center d-flex m-0"> <h4 class="align-items-center d-flex m-0">
@ -125,12 +125,12 @@
><gf-premium-indicator ><gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'" *ngIf="user?.subscription?.type === 'Basic'"
class="ml-1" class="ml-1"
></gf-premium-indicator> />
</h4> </h4>
<gf-rules <gf-rules
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder" [hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
[rules]="currencyClusterRiskRules" [rules]="currencyClusterRiskRules"
></gf-rules> />
</div> </div>
<div class="mb-4"> <div class="mb-4">
<h4 class="align-items-center d-flex m-0"> <h4 class="align-items-center d-flex m-0">
@ -138,12 +138,12 @@
><gf-premium-indicator ><gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'" *ngIf="user?.subscription?.type === 'Basic'"
class="ml-1" class="ml-1"
></gf-premium-indicator> />
</h4> </h4>
<gf-rules <gf-rules
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder" [hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
[rules]="accountClusterRiskRules" [rules]="accountClusterRiskRules"
></gf-rules> />
</div> </div>
<div> <div>
<h4 class="align-items-center d-flex m-0"> <h4 class="align-items-center d-flex m-0">
@ -151,12 +151,12 @@
><gf-premium-indicator ><gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'" *ngIf="user?.subscription?.type === 'Basic'"
class="ml-1" class="ml-1"
></gf-premium-indicator> />
</h4> </h4>
<gf-rules <gf-rules
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder" [hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
[rules]="feeRules" [rules]="feeRules"
></gf-rules> />
</div> </div>
</div> </div>
</div> </div>

View File

@ -86,16 +86,14 @@ export class HoldingsPageComponent implements OnDestroy, OnInit {
? $localize`Filter by account or tag...` ? $localize`Filter by account or tag...`
: ''; : '';
return this.dataService.fetchPortfolioDetails({ return this.fetchPortfolioDetails();
filters: this.activeFilters
});
}), }),
takeUntil(this.unsubscribeSubject) takeUntil(this.unsubscribeSubject)
) )
.subscribe((portfolioDetails) => { .subscribe((portfolioDetails) => {
this.portfolioDetails = portfolioDetails; this.portfolioDetails = portfolioDetails;
this.initializeAnalysisData(); this.initialize();
this.isLoading = false; this.isLoading = false;
@ -146,17 +144,41 @@ export class HoldingsPageComponent implements OnDestroy, OnInit {
...tagFilters ...tagFilters
]; ];
if (this.user?.settings?.isExperimentalFeatures === true) {
this.holdings = undefined;
this.fetchPortfolioDetails()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((portfolioDetails) => {
this.portfolioDetails = portfolioDetails;
this.initialize();
this.changeDetectorRef.markForCheck();
});
}
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
} }
}); });
} }
public initialize() { public ngOnDestroy() {
this.holdings = []; this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
} }
public initializeAnalysisData() { private fetchPortfolioDetails() {
this.initialize(); return this.dataService.fetchPortfolioDetails({
filters:
this.activeFilters.length > 0
? this.activeFilters
: this.userService.getFilters()
});
}
private initialize() {
this.holdings = [];
for (const [symbol, holding] of Object.entries( for (const [symbol, holding] of Object.entries(
this.portfolioDetails.holdings this.portfolioDetails.holdings
@ -165,11 +187,6 @@ export class HoldingsPageComponent implements OnDestroy, OnInit {
} }
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private openPositionDialog({ private openPositionDialog({
dataSource, dataSource,
symbol symbol

View File

@ -2,12 +2,14 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Holdings</h1> <h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Holdings</h1>
@if (!user?.settings?.isExperimentalFeatures) {
<gf-activities-filter <gf-activities-filter
[allFilters]="allFilters" [allFilters]="allFilters"
[isLoading]="isLoading" [isLoading]="isLoading"
[placeholder]="placeholder" [placeholder]="placeholder"
(valueChanged)="filters$.next($event)" (valueChanged)="filters$.next($event)"
></gf-activities-filter> />
}
</div> </div>
</div> </div>
<div class="row"> <div class="row">
@ -18,7 +20,7 @@
[hasPermissionToCreateActivity]="hasPermissionToCreateOrder" [hasPermissionToCreateActivity]="hasPermissionToCreateOrder"
[holdings]="holdings" [holdings]="holdings"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
></gf-holdings-table> />
<div <div
*ngIf="hasPermissionToCreateOrder && holdings?.length > 0" *ngIf="hasPermissionToCreateOrder && holdings?.length > 0"
class="text-center" class="text-center"

View File

@ -171,10 +171,7 @@
<div class="align-items-center d-flex mb-2"> <div class="align-items-center d-flex mb-2">
<h4 class="align-items-center d-flex flex-grow-1 m-0"> <h4 class="align-items-center d-flex flex-grow-1 m-0">
<span>Premium</span> <span>Premium</span>
<gf-premium-indicator <gf-premium-indicator class="ml-1" [enableLink]="false" />
class="ml-1"
[enableLink]="false"
></gf-premium-indicator>
</h4> </h4>
<div *ngIf="user?.subscription?.type === 'Premium'"> <div *ngIf="user?.subscription?.type === 'Premium'">
<ion-icon class="mr-1" name="checkmark-outline" /> <ion-icon class="mr-1" name="checkmark-outline" />

View File

@ -20,7 +20,7 @@
[keys]="['symbol']" [keys]="['symbol']"
[positions]="symbols" [positions]="symbols"
[showLabels]="deviceType !== 'mobile'" [showLabels]="deviceType !== 'mobile'"
></gf-portfolio-proportion-chart> />
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
</div> </div>
@ -35,7 +35,7 @@
[keys]="['currency']" [keys]="['currency']"
[maxItems]="10" [maxItems]="10"
[positions]="positions" [positions]="positions"
></gf-portfolio-proportion-chart> />
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
</div> </div>
@ -50,7 +50,7 @@
[keys]="['name']" [keys]="['name']"
[maxItems]="10" [maxItems]="10"
[positions]="sectors" [positions]="sectors"
></gf-portfolio-proportion-chart> />
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
</div> </div>
@ -64,7 +64,7 @@
[isInPercent]="true" [isInPercent]="true"
[keys]="['name']" [keys]="['name']"
[positions]="continents" [positions]="continents"
></gf-portfolio-proportion-chart> />
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
</div> </div>
@ -81,7 +81,7 @@
format="{0}%" format="{0}%"
[countries]="countries" [countries]="countries"
[isInPercent]="true" [isInPercent]="true"
></gf-world-map-chart> />
</div> </div>
<div class="row"> <div class="row">
<div class="col-xs-12 col-md my-2"> <div class="col-xs-12 col-md my-2">
@ -135,7 +135,7 @@
[hasPermissionToShowValues]="false" [hasPermissionToShowValues]="false"
[holdings]="holdings" [holdings]="holdings"
[pageSize]="7" [pageSize]="7"
></gf-holdings-table> />
</div> </div>
</div> </div>
<div class="row my-5"> <div class="row my-5">

View File

@ -9,7 +9,7 @@
<div <div
class="align-items-center d-flex flex-column justify-content-center w-100" class="align-items-center d-flex flex-column justify-content-center w-100"
> >
<gf-logo size="large"></gf-logo> <gf-logo size="large" />
<p class="lead m-0">Wealth Management Software</p> <p class="lead m-0">Wealth Management Software</p>
</div> </div>
</div> </div>

View File

@ -44,6 +44,7 @@ import { SumioPageComponent } from './products/sumio-page.component';
import { TillerPageComponent } from './products/tiller-page.component'; import { TillerPageComponent } from './products/tiller-page.component';
import { UtlunaPageComponent } from './products/utluna-page.component'; import { UtlunaPageComponent } from './products/utluna-page.component';
import { VyzerPageComponent } from './products/vyzer-page.component'; import { VyzerPageComponent } from './products/vyzer-page.component';
import { WealthfolioPageComponent } from './products/wealthfolio-page.component';
import { WealthicaPageComponent } from './products/wealthica-page.component'; import { WealthicaPageComponent } from './products/wealthica-page.component';
import { WhalPageComponent } from './products/whal-page.component'; import { WhalPageComponent } from './products/whal-page.component';
import { YeekateePageComponent } from './products/yeekatee-page.component'; import { YeekateePageComponent } from './products/yeekatee-page.component';
@ -528,6 +529,14 @@ export const products: Product[] = [
pricingPerYear: '$348', pricingPerYear: '$348',
slogan: 'Virtual Family Office for Smart Wealth Management' slogan: 'Virtual Family Office for Smart Wealth Management'
}, },
{
component: WealthfolioPageComponent,
hasSelfHostingAbility: true,
key: 'wealthfolio',
languages: ['English'],
name: 'Wealthfolio',
slogan: 'Desktop Investment Tracker'
},
{ {
component: WealthicaPageComponent, component: WealthicaPageComponent,
founded: 2015, founded: 2015,

View File

@ -0,0 +1,32 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({
host: { class: 'page' },
imports: [CommonModule, MatButtonModule, RouterModule],
selector: 'gf-wealthfolio-page',
standalone: true,
styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html'
})
export class WealthfolioPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => {
return key === 'ghostfolio';
});
public product2 = products.find(({ key }) => {
return key === 'wealthfolio';
});
public routerLinkAbout = ['/' + $localize`about`];
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkResourcesPersonalFinanceTools = [
'/' + $localize`resources`,
'personal-finance-tools'
];
}

View File

@ -4,7 +4,7 @@
<div <div
class="align-items-center d-flex flex-column justify-content-center mb-4 w-100" class="align-items-center d-flex flex-column justify-content-center mb-4 w-100"
> >
<gf-logo size="medium"></gf-logo> <gf-logo size="medium" />
</div> </div>
<div *ngIf="!hasError" class="col d-flex justify-content-center"> <div *ngIf="!hasError" class="col d-flex justify-content-center">

View File

@ -279,8 +279,14 @@ export class DataService {
return this.http.get<BenchmarkResponse>('/api/v1/benchmark'); return this.http.get<BenchmarkResponse>('/api/v1/benchmark');
} }
public fetchExport(activityIds?: string[]) { public fetchExport({
let params = new HttpParams(); activityIds,
filters
}: {
activityIds?: string[];
filters?: Filter[];
} = {}) {
let params = this.buildFiltersAsQueryParams({ filters });
if (activityIds) { if (activityIds) {
params = params.append('activityIds', activityIds.join(',')); params = params.append('activityIds', activityIds.join(','));

View File

@ -47,18 +47,26 @@ export class UserService extends ObservableStore<UserStoreState> {
} }
public getFilters() { public getFilters() {
const filters: Filter[] = [];
const user = this.getState().user; const user = this.getState().user;
return user?.settings?.isExperimentalFeatures === true if (user?.settings?.isExperimentalFeatures === true) {
? user.settings['filters.tags'] if (user.settings['filters.accounts']) {
? <Filter[]>[ filters.push({
{ id: user.settings['filters.accounts'][0],
id: user.settings['filters.tags'][0], type: 'ACCOUNT'
type: 'TAG' });
} }
]
: [] if (user.settings['filters.tags']) {
: []; filters.push({
id: user.settings['filters.tags'][0],
type: 'TAG'
});
}
}
return filters;
} }
public remove() { public remove() {

View File

@ -1,6 +1,16 @@
{ {
"createdAt": "2023-12-30T00:00:00.000Z", "createdAt": "2024-01-29T00:00:00.000Z",
"data": [ "data": [
{
"name": "Aptabase",
"description": "Analytics for Apps, open source, simple and privacy-friendly. SDKs for Swift, React Native, Electron, Flutter and many others.",
"href": "https://aptabase.com"
},
{
"name": "Argos",
"description": "Argos provides the developer tools to debug tests and detect visual regressions..",
"href": "https://argos-ci.com"
},
{ {
"name": "BoxyHQ", "name": "BoxyHQ",
"description": "BoxyHQs suite of APIs for security and privacy helps engineering teams build and ship compliant cloud applications faster.", "description": "BoxyHQs suite of APIs for security and privacy helps engineering teams build and ship compliant cloud applications faster.",
@ -81,6 +91,16 @@
"description": "Open source, end-to-end encrypted platform that lets you securely manage secrets and configs across your team, devices, and infrastructure.", "description": "Open source, end-to-end encrypted platform that lets you securely manage secrets and configs across your team, devices, and infrastructure.",
"href": "https://infisical.com" "href": "https://infisical.com"
}, },
{
"name": "Langfuse",
"description": "Open source LLM engineering platform. Debug, analyze and iterate together.",
"href": "https://langfuse.com"
},
{
"name": "Lost Pixel",
"description": "Open source visual regression testing alternative to Percy & Chromatic",
"href": "https://lost-pixel.com"
},
{ {
"name": "Mockoon", "name": "Mockoon",
"description": "Mockoon is the easiest and quickest way to design and run mock REST APIs.", "description": "Mockoon is the easiest and quickest way to design and run mock REST APIs.",

View File

@ -48,7 +48,7 @@
<link href="../assets/site.webmanifest" rel="manifest" /> <link href="../assets/site.webmanifest" rel="manifest" />
</head> </head>
<body> <body>
<gf-root></gf-root> <gf-root />
<script src="../ionicons/ionicons.esm.js" type="module"></script> <script src="../ionicons/ionicons.esm.js" type="module"></script>
<script nomodule="" src="ionicons.js"></script> <script nomodule="" src="ionicons.js"></script>

Some files were not shown because too many files have changed in this diff Show More