Compare commits
38 Commits
Author | SHA1 | Date | |
---|---|---|---|
9ad1c2177c | |||
15bf9f2f9c | |||
ebc5008569 | |||
37759ba03f | |||
8319b216bb | |||
782d131b0d | |||
72e75208df | |||
4b1c27c245 | |||
61f0da35bc | |||
80464c7846 | |||
74f4323903 | |||
127dbf9dcd | |||
66bdb374e8 | |||
4ad4fa2b30 | |||
1fd836194f | |||
2090db1199 | |||
053c7e591e | |||
9b5e350e3b | |||
378e57c3bc | |||
6765191a8c | |||
8438a45bcf | |||
30a64e7fc1 | |||
f2cb671c7f | |||
3f41e5c5de | |||
c1ad483f33 | |||
f3d961bc16 | |||
42b70ef568 | |||
77beaaba08 | |||
d9c07456cd | |||
0a53df4293 | |||
4416ba0c88 | |||
486de968a2 | |||
a5833566a8 | |||
261f5844dd | |||
2173c418a7 | |||
4efd5cefd8 | |||
d735e4db75 | |||
e10707fde4 |
70
CHANGELOG.md
70
CHANGELOG.md
@ -5,7 +5,75 @@ 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.77.0 - 2024-04-27
|
## 2.81.0 - 2024-05-12
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added an indicator for active filters (experimental)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the delete all activities functionality on the portfolio activities page to work with the filters of the assistant
|
||||||
|
- Improved the language localization for German (`de`)
|
||||||
|
- Improved the language localization for Türkçe (`tr`)
|
||||||
|
- Upgraded `Nx` from version `18.3.3` to `19.0.2`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the position detail dialog close functionality
|
||||||
|
|
||||||
|
## 2.80.0 - 2024-05-08
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the absolute change column to the holdings table on the home page
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Increased the spacing around the floating action buttons (FAB)
|
||||||
|
- Set the icon column of the activities table to stick at the beginning
|
||||||
|
- Set the icon column of the holdings table to stick at the beginning
|
||||||
|
- Increased the number of attempts of queue jobs from `10` to `12` (fail later)
|
||||||
|
- Upgraded `ionicons` from version `7.3.0` to `7.4.0`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the position detail dialog open functionality when searching for a holding in the assistant
|
||||||
|
|
||||||
|
## 2.79.0 - 2024-05-04
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Moved the holdings table to the holdings tab of the home page
|
||||||
|
- Improved the performance labels (with and without currency effects) in the position detail dialog
|
||||||
|
- Optimized the calculations of the portfolio details endpoint
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue with the benchmarks in the markets overview
|
||||||
|
- Fixed an issue with the _Fear & Greed Index_ (market mood) in the markets overview
|
||||||
|
|
||||||
|
## 2.78.0 - 2024-05-02
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added a form validation against the DTO in the create or update access dialog
|
||||||
|
- Added a form validation against the DTO in the asset profile details dialog of the admin control
|
||||||
|
- Added a form validation against the DTO in the platform management of the admin control panel
|
||||||
|
- Added a form validation against the DTO in the tag management of the admin control panel
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Set the performance column of the holdings table to stick at the end
|
||||||
|
- Skipped the caching in the portfolio calculator if there are active filters (experimental)
|
||||||
|
- Improved the `INACTIVE` user role
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue in the calculation of the portfolio summary caused by future liabilities
|
||||||
|
- Fixed a division by zero error in the dividend yield calculation (experimental)
|
||||||
|
|
||||||
|
## 2.77.1 - 2024-04-27
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
36
README.md
36
README.md
@ -85,23 +85,23 @@ We provide official container images hosted on [Docker Hub](https://hub.docker.c
|
|||||||
|
|
||||||
### Supported Environment Variables
|
### Supported Environment Variables
|
||||||
|
|
||||||
| Name | Default Value | Description |
|
| Name | Type | Default Value | Description |
|
||||||
| ------------------------ | ------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------------ | ------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `ACCESS_TOKEN_SALT` | | A random string used as salt for access tokens |
|
| `ACCESS_TOKEN_SALT` | string | | A random string used as salt for access tokens |
|
||||||
| `API_KEY_COINGECKO_DEMO` | | The _CoinGecko_ Demo API key |
|
| `API_KEY_COINGECKO_DEMO` | string (`optional`) | | The _CoinGecko_ Demo API key |
|
||||||
| `API_KEY_COINGECKO_PRO` | | The _CoinGecko_ Pro API |
|
| `API_KEY_COINGECKO_PRO` | string (`optional`) | | The _CoinGecko_ Pro API |
|
||||||
| `DATABASE_URL` | | The database connection URL, e.g. `postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?sslmode=prefer` |
|
| `DATABASE_URL` | string | | The database connection URL, e.g. `postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?sslmode=prefer` |
|
||||||
| `HOST` | `0.0.0.0` | The host where the Ghostfolio application will run on |
|
| `HOST` | string (`optional`) | `0.0.0.0` | The host where the Ghostfolio application will run on |
|
||||||
| `JWT_SECRET_KEY` | | A random string used for _JSON Web Tokens_ (JWT) |
|
| `JWT_SECRET_KEY` | string | | A random string used for _JSON Web Tokens_ (JWT) |
|
||||||
| `PORT` | `3333` | The port where the Ghostfolio application will run on |
|
| `PORT` | number (`optional`) | `3333` | The port where the Ghostfolio application will run on |
|
||||||
| `POSTGRES_DB` | | The name of the _PostgreSQL_ database |
|
| `POSTGRES_DB` | string | | The name of the _PostgreSQL_ database |
|
||||||
| `POSTGRES_PASSWORD` | | The password of the _PostgreSQL_ database |
|
| `POSTGRES_PASSWORD` | string | | The password of the _PostgreSQL_ database |
|
||||||
| `POSTGRES_USER` | | The user of the _PostgreSQL_ database |
|
| `POSTGRES_USER` | string | | The user of the _PostgreSQL_ database |
|
||||||
| `REDIS_DB` | `0` | The database index of _Redis_ |
|
| `REDIS_DB` | number (`optional`) | `0` | The database index of _Redis_ |
|
||||||
| `REDIS_HOST` | | The host where _Redis_ is running |
|
| `REDIS_HOST` | string | | The host where _Redis_ is running |
|
||||||
| `REDIS_PASSWORD` | | The password of _Redis_ |
|
| `REDIS_PASSWORD` | string | | The password of _Redis_ |
|
||||||
| `REDIS_PORT` | | The port where _Redis_ is running |
|
| `REDIS_PORT` | number | | The port where _Redis_ is running |
|
||||||
| `REQUEST_TIMEOUT` | `2000` | The timeout of network requests to data providers in milliseconds |
|
| `REQUEST_TIMEOUT` | number (`optional`) | `2000` | The timeout of network requests to data providers in milliseconds |
|
||||||
|
|
||||||
### Run with Docker Compose
|
### Run with Docker Compose
|
||||||
|
|
||||||
@ -142,7 +142,7 @@ docker compose --env-file ./.env -f docker/docker-compose.build.yml up -d
|
|||||||
|
|
||||||
### Home Server Systems (Community)
|
### Home Server Systems (Community)
|
||||||
|
|
||||||
Ghostfolio is available for various home server systems, including [CasaOS](https://github.com/bigbeartechworld/big-bear-casaos), Home Assistant, [Runtipi](https://www.runtipi.io/docs/apps-available), [TrueCharts](https://truecharts.org/charts/stable/ghostfolio), [Umbrel](https://apps.umbrel.com/app/ghostfolio), and [Unraid](https://unraid.net/community/apps?q=ghostfolio).
|
Ghostfolio is available for various home server systems, including [CasaOS](https://github.com/bigbeartechworld/big-bear-casaos), [Home Assistant](https://github.com/lildude/ha-addon-ghostfolio), [Runtipi](https://www.runtipi.io/docs/apps-available), [TrueCharts](https://truecharts.org/charts/stable/ghostfolio), [Umbrel](https://apps.umbrel.com/app/ghostfolio), and [Unraid](https://unraid.net/community/apps?q=ghostfolio).
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
@ -2,10 +2,12 @@ import { UserService } from '@ghostfolio/api/app/user/user.service';
|
|||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||||
import { HEADER_KEY_TIMEZONE } from '@ghostfolio/common/config';
|
import { HEADER_KEY_TIMEZONE } from '@ghostfolio/common/config';
|
||||||
|
import { hasRole } from '@ghostfolio/common/permissions';
|
||||||
|
|
||||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
import { HttpException, Injectable } from '@nestjs/common';
|
||||||
import { PassportStrategy } from '@nestjs/passport';
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
import * as countriesAndTimezones from 'countries-and-timezones';
|
import * as countriesAndTimezones from 'countries-and-timezones';
|
||||||
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -29,6 +31,13 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
|||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
||||||
|
if (hasRole(user, 'INACTIVE')) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
|
||||||
|
StatusCodes.TOO_MANY_REQUESTS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const country =
|
const country =
|
||||||
countriesAndTimezones.getCountryForTimezone(timezone)?.id;
|
countriesAndTimezones.getCountryForTimezone(timezone)?.id;
|
||||||
|
|
||||||
@ -45,10 +54,20 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
|||||||
|
|
||||||
return user;
|
return user;
|
||||||
} else {
|
} else {
|
||||||
throw '';
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.NOT_FOUND),
|
||||||
|
StatusCodes.NOT_FOUND
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error?.getStatus() === StatusCodes.TOO_MANY_REQUESTS) {
|
||||||
|
throw error;
|
||||||
|
} else {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.UNAUTHORIZED),
|
||||||
|
StatusCodes.UNAUTHORIZED
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
throw new UnauthorizedException('unauthorized', err.message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ import {
|
|||||||
DATA_GATHERING_QUEUE_PRIORITY_HIGH,
|
DATA_GATHERING_QUEUE_PRIORITY_HIGH,
|
||||||
HEADER_KEY_IMPERSONATION
|
HEADER_KEY_IMPERSONATION
|
||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { permissions } from '@ghostfolio/common/permissions';
|
||||||
import type { DateRange, RequestWithUser } from '@ghostfolio/common/types';
|
import type { DateRange, RequestWithUser } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -53,8 +53,20 @@ export class OrderController {
|
|||||||
@Delete()
|
@Delete()
|
||||||
@HasPermission(permissions.deleteOrder)
|
@HasPermission(permissions.deleteOrder)
|
||||||
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
||||||
public async deleteOrders(): Promise<number> {
|
public async deleteOrders(
|
||||||
|
@Query('accounts') filterByAccounts?: string,
|
||||||
|
@Query('assetClasses') filterByAssetClasses?: string,
|
||||||
|
@Query('tags') filterByTags?: string
|
||||||
|
): Promise<number> {
|
||||||
|
const filters = this.apiService.buildFiltersFromQueryParams({
|
||||||
|
filterByAccounts,
|
||||||
|
filterByAssetClasses,
|
||||||
|
filterByTags
|
||||||
|
});
|
||||||
|
|
||||||
return this.orderService.deleteOrders({
|
return this.orderService.deleteOrders({
|
||||||
|
filters,
|
||||||
|
userCurrency: this.request.user.Settings.settings.baseCurrency,
|
||||||
userId: this.request.user.id
|
userId: this.request.user.id
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -88,21 +100,26 @@ export class OrderController {
|
|||||||
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId,
|
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId,
|
||||||
@Query('accounts') filterByAccounts?: string,
|
@Query('accounts') filterByAccounts?: string,
|
||||||
@Query('assetClasses') filterByAssetClasses?: string,
|
@Query('assetClasses') filterByAssetClasses?: string,
|
||||||
@Query('range') dateRange: DateRange = 'max',
|
@Query('range') dateRange?: DateRange,
|
||||||
@Query('skip') skip?: number,
|
@Query('skip') skip?: number,
|
||||||
@Query('sortColumn') sortColumn?: string,
|
@Query('sortColumn') sortColumn?: string,
|
||||||
@Query('sortDirection') sortDirection?: Prisma.SortOrder,
|
@Query('sortDirection') sortDirection?: Prisma.SortOrder,
|
||||||
@Query('tags') filterByTags?: string,
|
@Query('tags') filterByTags?: string,
|
||||||
@Query('take') take?: number
|
@Query('take') take?: number
|
||||||
): Promise<Activities> {
|
): Promise<Activities> {
|
||||||
|
let endDate: Date;
|
||||||
|
let startDate: Date;
|
||||||
|
|
||||||
|
if (dateRange) {
|
||||||
|
({ endDate, startDate } = getInterval(dateRange));
|
||||||
|
}
|
||||||
|
|
||||||
const filters = this.apiService.buildFiltersFromQueryParams({
|
const filters = this.apiService.buildFiltersFromQueryParams({
|
||||||
filterByAccounts,
|
filterByAccounts,
|
||||||
filterByAssetClasses,
|
filterByAssetClasses,
|
||||||
filterByTags
|
filterByTags
|
||||||
});
|
});
|
||||||
|
|
||||||
const { endDate, startDate } = getInterval(dateRange);
|
|
||||||
|
|
||||||
const impersonationUserId =
|
const impersonationUserId =
|
||||||
await this.impersonationService.validateImpersonationId(impersonationId);
|
await this.impersonationService.validateImpersonationId(impersonationId);
|
||||||
const userCurrency = this.request.user.Settings.settings.baseCurrency;
|
const userCurrency = this.request.user.Settings.settings.baseCurrency;
|
||||||
|
@ -194,16 +194,36 @@ export class OrderService {
|
|||||||
return order;
|
return order;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteOrders(where: Prisma.OrderWhereInput): Promise<number> {
|
public async deleteOrders({
|
||||||
|
filters,
|
||||||
|
userCurrency,
|
||||||
|
userId
|
||||||
|
}: {
|
||||||
|
filters?: Filter[];
|
||||||
|
userCurrency: string;
|
||||||
|
userId: string;
|
||||||
|
}): Promise<number> {
|
||||||
|
const { activities } = await this.getOrders({
|
||||||
|
filters,
|
||||||
|
userId,
|
||||||
|
userCurrency,
|
||||||
|
includeDrafts: true,
|
||||||
|
withExcludedAccounts: true
|
||||||
|
});
|
||||||
|
|
||||||
const { count } = await this.prismaService.order.deleteMany({
|
const { count } = await this.prismaService.order.deleteMany({
|
||||||
where
|
where: {
|
||||||
|
id: {
|
||||||
|
in: activities.map(({ id }) => {
|
||||||
|
return id;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.eventEmitter.emit(
|
this.eventEmitter.emit(
|
||||||
PortfolioChangedEvent.getName(),
|
PortfolioChangedEvent.getName(),
|
||||||
new PortfolioChangedEvent({
|
new PortfolioChangedEvent({ userId })
|
||||||
userId: <string>where.userId
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return count;
|
return count;
|
||||||
|
@ -32,6 +32,7 @@ export class PortfolioCalculatorFactory {
|
|||||||
calculationType,
|
calculationType,
|
||||||
currency,
|
currency,
|
||||||
dateRange = 'max',
|
dateRange = 'max',
|
||||||
|
hasFilters,
|
||||||
isExperimentalFeatures = false,
|
isExperimentalFeatures = false,
|
||||||
userId
|
userId
|
||||||
}: {
|
}: {
|
||||||
@ -40,9 +41,12 @@ export class PortfolioCalculatorFactory {
|
|||||||
calculationType: PerformanceCalculationType;
|
calculationType: PerformanceCalculationType;
|
||||||
currency: string;
|
currency: string;
|
||||||
dateRange?: DateRange;
|
dateRange?: DateRange;
|
||||||
|
hasFilters: boolean;
|
||||||
isExperimentalFeatures?: boolean;
|
isExperimentalFeatures?: boolean;
|
||||||
userId: string;
|
userId: string;
|
||||||
}): PortfolioCalculator {
|
}): PortfolioCalculator {
|
||||||
|
const useCache = !hasFilters && isExperimentalFeatures;
|
||||||
|
|
||||||
switch (calculationType) {
|
switch (calculationType) {
|
||||||
case PerformanceCalculationType.MWR:
|
case PerformanceCalculationType.MWR:
|
||||||
return new MWRPortfolioCalculator({
|
return new MWRPortfolioCalculator({
|
||||||
@ -50,7 +54,7 @@ export class PortfolioCalculatorFactory {
|
|||||||
activities,
|
activities,
|
||||||
currency,
|
currency,
|
||||||
dateRange,
|
dateRange,
|
||||||
isExperimentalFeatures,
|
useCache,
|
||||||
userId,
|
userId,
|
||||||
configurationService: this.configurationService,
|
configurationService: this.configurationService,
|
||||||
currentRateService: this.currentRateService,
|
currentRateService: this.currentRateService,
|
||||||
@ -64,7 +68,7 @@ export class PortfolioCalculatorFactory {
|
|||||||
currency,
|
currency,
|
||||||
currentRateService: this.currentRateService,
|
currentRateService: this.currentRateService,
|
||||||
dateRange,
|
dateRange,
|
||||||
isExperimentalFeatures,
|
useCache,
|
||||||
userId,
|
userId,
|
||||||
configurationService: this.configurationService,
|
configurationService: this.configurationService,
|
||||||
exchangeRateDataService: this.exchangeRateDataService,
|
exchangeRateDataService: this.exchangeRateDataService,
|
||||||
|
@ -37,6 +37,7 @@ import {
|
|||||||
eachDayOfInterval,
|
eachDayOfInterval,
|
||||||
endOfDay,
|
endOfDay,
|
||||||
format,
|
format,
|
||||||
|
isAfter,
|
||||||
isBefore,
|
isBefore,
|
||||||
isSameDay,
|
isSameDay,
|
||||||
max,
|
max,
|
||||||
@ -44,26 +45,26 @@ import {
|
|||||||
subDays
|
subDays
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import { first, last, uniq, uniqBy } from 'lodash';
|
import { first, last, uniq, uniqBy } from 'lodash';
|
||||||
import ms from 'ms';
|
|
||||||
|
|
||||||
export abstract class PortfolioCalculator {
|
export abstract class PortfolioCalculator {
|
||||||
protected static readonly ENABLE_LOGGING = false;
|
protected static readonly ENABLE_LOGGING = false;
|
||||||
|
|
||||||
protected accountBalanceItems: HistoricalDataItem[];
|
protected accountBalanceItems: HistoricalDataItem[];
|
||||||
protected orders: PortfolioOrder[];
|
protected activities: PortfolioOrder[];
|
||||||
|
|
||||||
private configurationService: ConfigurationService;
|
private configurationService: ConfigurationService;
|
||||||
private currency: string;
|
private currency: string;
|
||||||
private currentRateService: CurrentRateService;
|
private currentRateService: CurrentRateService;
|
||||||
private dataProviderInfos: DataProviderInfo[];
|
private dataProviderInfos: DataProviderInfo[];
|
||||||
|
private dateRange: DateRange;
|
||||||
private endDate: Date;
|
private endDate: Date;
|
||||||
private exchangeRateDataService: ExchangeRateDataService;
|
private exchangeRateDataService: ExchangeRateDataService;
|
||||||
private isExperimentalFeatures: boolean;
|
|
||||||
private redisCacheService: RedisCacheService;
|
private redisCacheService: RedisCacheService;
|
||||||
private snapshot: PortfolioSnapshot;
|
private snapshot: PortfolioSnapshot;
|
||||||
private snapshotPromise: Promise<void>;
|
private snapshotPromise: Promise<void>;
|
||||||
private startDate: Date;
|
private startDate: Date;
|
||||||
private transactionPoints: TransactionPoint[];
|
private transactionPoints: TransactionPoint[];
|
||||||
|
private useCache: boolean;
|
||||||
private userId: string;
|
private userId: string;
|
||||||
|
|
||||||
public constructor({
|
public constructor({
|
||||||
@ -74,8 +75,8 @@ export abstract class PortfolioCalculator {
|
|||||||
currentRateService,
|
currentRateService,
|
||||||
dateRange,
|
dateRange,
|
||||||
exchangeRateDataService,
|
exchangeRateDataService,
|
||||||
isExperimentalFeatures,
|
|
||||||
redisCacheService,
|
redisCacheService,
|
||||||
|
useCache,
|
||||||
userId
|
userId
|
||||||
}: {
|
}: {
|
||||||
accountBalanceItems: HistoricalDataItem[];
|
accountBalanceItems: HistoricalDataItem[];
|
||||||
@ -85,18 +86,18 @@ export abstract class PortfolioCalculator {
|
|||||||
currentRateService: CurrentRateService;
|
currentRateService: CurrentRateService;
|
||||||
dateRange: DateRange;
|
dateRange: DateRange;
|
||||||
exchangeRateDataService: ExchangeRateDataService;
|
exchangeRateDataService: ExchangeRateDataService;
|
||||||
isExperimentalFeatures: boolean;
|
|
||||||
redisCacheService: RedisCacheService;
|
redisCacheService: RedisCacheService;
|
||||||
|
useCache: boolean;
|
||||||
userId: string;
|
userId: string;
|
||||||
}) {
|
}) {
|
||||||
this.accountBalanceItems = accountBalanceItems;
|
this.accountBalanceItems = accountBalanceItems;
|
||||||
this.configurationService = configurationService;
|
this.configurationService = configurationService;
|
||||||
this.currency = currency;
|
this.currency = currency;
|
||||||
this.currentRateService = currentRateService;
|
this.currentRateService = currentRateService;
|
||||||
|
this.dateRange = dateRange;
|
||||||
this.exchangeRateDataService = exchangeRateDataService;
|
this.exchangeRateDataService = exchangeRateDataService;
|
||||||
this.isExperimentalFeatures = isExperimentalFeatures;
|
|
||||||
|
|
||||||
this.orders = activities
|
this.activities = activities
|
||||||
.map(
|
.map(
|
||||||
({
|
({
|
||||||
date,
|
date,
|
||||||
@ -107,6 +108,12 @@ export abstract class PortfolioCalculator {
|
|||||||
type,
|
type,
|
||||||
unitPrice
|
unitPrice
|
||||||
}) => {
|
}) => {
|
||||||
|
if (isAfter(date, new Date(Date.now()))) {
|
||||||
|
// Adapt date to today if activity is in future (e.g. liability)
|
||||||
|
// to include it in the interval
|
||||||
|
date = endOfDay(new Date(Date.now()));
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
SymbolProfile,
|
SymbolProfile,
|
||||||
tags,
|
tags,
|
||||||
@ -123,6 +130,7 @@ export abstract class PortfolioCalculator {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.redisCacheService = redisCacheService;
|
this.redisCacheService = redisCacheService;
|
||||||
|
this.useCache = useCache;
|
||||||
this.userId = userId;
|
this.userId = userId;
|
||||||
|
|
||||||
const { endDate, startDate } = getInterval(dateRange);
|
const { endDate, startDate } = getInterval(dateRange);
|
||||||
@ -917,7 +925,7 @@ export abstract class PortfolioCalculator {
|
|||||||
tags,
|
tags,
|
||||||
type,
|
type,
|
||||||
unitPrice
|
unitPrice
|
||||||
} of this.orders) {
|
} of this.activities) {
|
||||||
let currentTransactionPointItem: TransactionPointSymbol;
|
let currentTransactionPointItem: TransactionPointSymbol;
|
||||||
const oldAccumulatedSymbol = symbols[SymbolProfile.symbol];
|
const oldAccumulatedSymbol = symbols[SymbolProfile.symbol];
|
||||||
|
|
||||||
@ -1041,11 +1049,13 @@ export abstract class PortfolioCalculator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async initialize() {
|
private async initialize() {
|
||||||
if (this.isExperimentalFeatures) {
|
if (this.useCache) {
|
||||||
const startTimeTotal = performance.now();
|
const startTimeTotal = performance.now();
|
||||||
|
|
||||||
const cachedSnapshot = await this.redisCacheService.get(
|
const cachedSnapshot = await this.redisCacheService.get(
|
||||||
this.redisCacheService.getPortfolioSnapshotKey(this.userId)
|
this.redisCacheService.getPortfolioSnapshotKey({
|
||||||
|
userId: this.userId
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
if (cachedSnapshot) {
|
if (cachedSnapshot) {
|
||||||
@ -1068,7 +1078,9 @@ export abstract class PortfolioCalculator {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.redisCacheService.set(
|
this.redisCacheService.set(
|
||||||
this.redisCacheService.getPortfolioSnapshotKey(this.userId),
|
this.redisCacheService.getPortfolioSnapshotKey({
|
||||||
|
userId: this.userId
|
||||||
|
}),
|
||||||
JSON.stringify(this.snapshot),
|
JSON.stringify(this.snapshot),
|
||||||
this.configurationService.get('CACHE_QUOTES_TTL')
|
this.configurationService.get('CACHE_QUOTES_TTL')
|
||||||
);
|
);
|
||||||
|
@ -123,6 +123,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
activities,
|
activities,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'CHF',
|
currency: 'CHF',
|
||||||
|
hasFilters: false,
|
||||||
userId: userDummyData.id
|
userId: userDummyData.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -108,6 +108,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
activities,
|
activities,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'CHF',
|
currency: 'CHF',
|
||||||
|
hasFilters: false,
|
||||||
userId: userDummyData.id
|
userId: userDummyData.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -93,6 +93,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
activities,
|
activities,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'CHF',
|
currency: 'CHF',
|
||||||
|
hasFilters: false,
|
||||||
userId: userDummyData.id
|
userId: userDummyData.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -121,6 +121,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
activities,
|
activities,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'CHF',
|
currency: 'CHF',
|
||||||
|
hasFilters: false,
|
||||||
userId: userDummyData.id
|
userId: userDummyData.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -93,6 +93,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
activities,
|
activities,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'USD',
|
currency: 'USD',
|
||||||
|
hasFilters: false,
|
||||||
userId: userDummyData.id
|
userId: userDummyData.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -106,6 +106,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
activities,
|
activities,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'CHF',
|
currency: 'CHF',
|
||||||
|
hasFilters: false,
|
||||||
userId: userDummyData.id
|
userId: userDummyData.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -93,6 +93,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
activities,
|
activities,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'USD',
|
currency: 'USD',
|
||||||
|
hasFilters: false,
|
||||||
userId: userDummyData.id
|
userId: userDummyData.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
const activities: Activity[] = [
|
const activities: Activity[] = [
|
||||||
{
|
{
|
||||||
...activityDummyData,
|
...activityDummyData,
|
||||||
date: new Date('2022-01-01'),
|
date: new Date('2023-01-01'), // Date in future
|
||||||
fee: 0,
|
fee: 0,
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
SymbolProfile: {
|
SymbolProfile: {
|
||||||
@ -93,64 +93,16 @@ describe('PortfolioCalculator', () => {
|
|||||||
activities,
|
activities,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'USD',
|
currency: 'USD',
|
||||||
|
hasFilters: false,
|
||||||
userId: userDummyData.id
|
userId: userDummyData.id
|
||||||
});
|
});
|
||||||
|
|
||||||
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
|
|
||||||
parseDate('2022-01-01')
|
|
||||||
);
|
|
||||||
|
|
||||||
spy.mockRestore();
|
spy.mockRestore();
|
||||||
|
|
||||||
expect(portfolioSnapshot).toEqual({
|
const liabilitiesInBaseCurrency =
|
||||||
currentValueInBaseCurrency: new Big('0'),
|
await portfolioCalculator.getLiabilitiesInBaseCurrency();
|
||||||
errors: [],
|
|
||||||
grossPerformance: new Big('0'),
|
expect(liabilitiesInBaseCurrency).toEqual(new Big(3000));
|
||||||
grossPerformancePercentage: new Big('0'),
|
|
||||||
grossPerformancePercentageWithCurrencyEffect: new Big('0'),
|
|
||||||
grossPerformanceWithCurrencyEffect: new Big('0'),
|
|
||||||
hasErrors: true,
|
|
||||||
netPerformance: new Big('0'),
|
|
||||||
netPerformancePercentage: new Big('0'),
|
|
||||||
netPerformancePercentageWithCurrencyEffect: new Big('0'),
|
|
||||||
netPerformanceWithCurrencyEffect: new Big('0'),
|
|
||||||
positions: [
|
|
||||||
{
|
|
||||||
averagePrice: new Big('3000'),
|
|
||||||
currency: 'USD',
|
|
||||||
dataSource: 'MANUAL',
|
|
||||||
dividend: new Big('0'),
|
|
||||||
dividendInBaseCurrency: new Big('0'),
|
|
||||||
fee: new Big('0'),
|
|
||||||
firstBuyDate: '2022-01-01',
|
|
||||||
grossPerformance: null,
|
|
||||||
grossPerformancePercentage: null,
|
|
||||||
grossPerformancePercentageWithCurrencyEffect: null,
|
|
||||||
grossPerformanceWithCurrencyEffect: null,
|
|
||||||
investment: new Big('0'),
|
|
||||||
investmentWithCurrencyEffect: new Big('0'),
|
|
||||||
marketPrice: null,
|
|
||||||
marketPriceInBaseCurrency: 3000,
|
|
||||||
netPerformance: null,
|
|
||||||
netPerformancePercentage: null,
|
|
||||||
netPerformancePercentageWithCurrencyEffect: null,
|
|
||||||
netPerformanceWithCurrencyEffect: null,
|
|
||||||
quantity: new Big('0'),
|
|
||||||
symbol: '55196015-1365-4560-aa60-8751ae6d18f8',
|
|
||||||
tags: [],
|
|
||||||
timeWeightedInvestment: new Big('0'),
|
|
||||||
timeWeightedInvestmentWithCurrencyEffect: new Big('0'),
|
|
||||||
transactionCount: 1,
|
|
||||||
valueInBaseCurrency: new Big('0')
|
|
||||||
}
|
|
||||||
],
|
|
||||||
totalFeesWithCurrencyEffect: new Big('0'),
|
|
||||||
totalInterestWithCurrencyEffect: new Big('0'),
|
|
||||||
totalInvestment: new Big('0'),
|
|
||||||
totalInvestmentWithCurrencyEffect: new Big('0'),
|
|
||||||
totalLiabilitiesWithCurrencyEffect: new Big('0'),
|
|
||||||
totalValuablesWithCurrencyEffect: new Big('0')
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -121,6 +121,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
activities,
|
activities,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'USD',
|
currency: 'USD',
|
||||||
|
hasFilters: false,
|
||||||
userId: userDummyData.id
|
userId: userDummyData.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -71,6 +71,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
activities: [],
|
activities: [],
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'CHF',
|
currency: 'CHF',
|
||||||
|
hasFilters: false,
|
||||||
userId: userDummyData.id
|
userId: userDummyData.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -108,6 +108,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
activities,
|
activities,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'CHF',
|
currency: 'CHF',
|
||||||
|
hasFilters: false,
|
||||||
userId: userDummyData.id
|
userId: userDummyData.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -108,6 +108,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
activities,
|
activities,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: 'CHF',
|
currency: 'CHF',
|
||||||
|
hasFilters: false,
|
||||||
userId: userDummyData.id
|
userId: userDummyData.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -203,7 +203,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
|||||||
let valueAtStartDateWithCurrencyEffect: Big;
|
let valueAtStartDateWithCurrencyEffect: Big;
|
||||||
|
|
||||||
// Clone orders to keep the original values in this.orders
|
// Clone orders to keep the original values in this.orders
|
||||||
let orders: PortfolioOrderItem[] = cloneDeep(this.orders).filter(
|
let orders: PortfolioOrderItem[] = cloneDeep(this.activities).filter(
|
||||||
({ SymbolProfile }) => {
|
({ SymbolProfile }) => {
|
||||||
return SymbolProfile.symbol === symbol;
|
return SymbolProfile.symbol === symbol;
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,13 @@ import { GetValuesParams } from './interfaces/get-values-params.interface';
|
|||||||
|
|
||||||
function mockGetValue(symbol: string, date: Date) {
|
function mockGetValue(symbol: string, date: Date) {
|
||||||
switch (symbol) {
|
switch (symbol) {
|
||||||
|
case '55196015-1365-4560-aa60-8751ae6d18f8':
|
||||||
|
if (isSameDay(parseDate('2022-01-31'), date)) {
|
||||||
|
return { marketPrice: 3000 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { marketPrice: 0 };
|
||||||
|
|
||||||
case 'BALN.SW':
|
case 'BALN.SW':
|
||||||
if (isSameDay(parseDate('2021-11-12'), date)) {
|
if (isSameDay(parseDate('2021-11-12'), date)) {
|
||||||
return { marketPrice: 146 };
|
return { marketPrice: 146 };
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
import { Position } from '@ghostfolio/common/interfaces';
|
|
||||||
|
|
||||||
export interface PortfolioPositions {
|
|
||||||
positions: Position[];
|
|
||||||
}
|
|
@ -52,7 +52,6 @@ import { Big } from 'big.js';
|
|||||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
|
||||||
import { PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface';
|
import { PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface';
|
||||||
import { PortfolioPositions } from './interfaces/portfolio-positions.interface';
|
|
||||||
import { PortfolioService } from './portfolio.service';
|
import { PortfolioService } from './portfolio.service';
|
||||||
|
|
||||||
@Controller('portfolio')
|
@Controller('portfolio')
|
||||||
@ -165,21 +164,21 @@ export class PortfolioController {
|
|||||||
portfolioSummary = nullifyValuesInObject(summary, [
|
portfolioSummary = nullifyValuesInObject(summary, [
|
||||||
'cash',
|
'cash',
|
||||||
'committedFunds',
|
'committedFunds',
|
||||||
'currentGrossPerformance',
|
|
||||||
'currentGrossPerformanceWithCurrencyEffect',
|
|
||||||
'currentNetPerformance',
|
|
||||||
'currentNetPerformanceWithCurrencyEffect',
|
|
||||||
'currentNetWorth',
|
'currentNetWorth',
|
||||||
'currentValue',
|
'currentValueInBaseCurrency',
|
||||||
'dividendInBaseCurrency',
|
'dividendInBaseCurrency',
|
||||||
'emergencyFund',
|
'emergencyFund',
|
||||||
'excludedAccountsAndActivities',
|
'excludedAccountsAndActivities',
|
||||||
'fees',
|
'fees',
|
||||||
'filteredValueInBaseCurrency',
|
'filteredValueInBaseCurrency',
|
||||||
'fireWealth',
|
'fireWealth',
|
||||||
|
'grossPerformance',
|
||||||
|
'grossPerformanceWithCurrencyEffect',
|
||||||
'interest',
|
'interest',
|
||||||
'items',
|
'items',
|
||||||
'liabilities',
|
'liabilities',
|
||||||
|
'netPerformance',
|
||||||
|
'netPerformanceWithCurrencyEffect',
|
||||||
'totalBuy',
|
'totalBuy',
|
||||||
'totalInvestment',
|
'totalInvestment',
|
||||||
'totalSell',
|
'totalSell',
|
||||||
@ -449,10 +448,14 @@ export class PortfolioController {
|
|||||||
.div(performanceInformation.performance.totalInvestment)
|
.div(performanceInformation.performance.totalInvestment)
|
||||||
.toNumber(),
|
.toNumber(),
|
||||||
valueInPercentage:
|
valueInPercentage:
|
||||||
performanceInformation.performance.currentValue === 0
|
performanceInformation.performance.currentValueInBaseCurrency ===
|
||||||
|
0
|
||||||
? 0
|
? 0
|
||||||
: new Big(value)
|
: new Big(value)
|
||||||
.div(performanceInformation.performance.currentValue)
|
.div(
|
||||||
|
performanceInformation.performance
|
||||||
|
.currentValueInBaseCurrency
|
||||||
|
)
|
||||||
.toNumber()
|
.toNumber()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -461,12 +464,12 @@ export class PortfolioController {
|
|||||||
performanceInformation.performance = nullifyValuesInObject(
|
performanceInformation.performance = nullifyValuesInObject(
|
||||||
performanceInformation.performance,
|
performanceInformation.performance,
|
||||||
[
|
[
|
||||||
'currentGrossPerformance',
|
|
||||||
'currentGrossPerformanceWithCurrencyEffect',
|
|
||||||
'currentNetPerformance',
|
|
||||||
'currentNetPerformanceWithCurrencyEffect',
|
|
||||||
'currentNetWorth',
|
'currentNetWorth',
|
||||||
'currentValue',
|
'currentValueInBaseCurrency',
|
||||||
|
'grossPerformance',
|
||||||
|
'grossPerformanceWithCurrencyEffect',
|
||||||
|
'netPerformance',
|
||||||
|
'netPerformanceWithCurrencyEffect',
|
||||||
'totalInvestment'
|
'totalInvestment'
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@ -483,39 +486,13 @@ export class PortfolioController {
|
|||||||
);
|
);
|
||||||
performanceInformation.performance = nullifyValuesInObject(
|
performanceInformation.performance = nullifyValuesInObject(
|
||||||
performanceInformation.performance,
|
performanceInformation.performance,
|
||||||
['currentNetPerformance', 'currentNetPerformancePercent']
|
['netPerformance']
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return performanceInformation;
|
return performanceInformation;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('positions')
|
|
||||||
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
|
||||||
@UseInterceptors(RedactValuesInResponseInterceptor)
|
|
||||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
|
||||||
public async getPositions(
|
|
||||||
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
|
|
||||||
@Query('accounts') filterByAccounts?: string,
|
|
||||||
@Query('assetClasses') filterByAssetClasses?: string,
|
|
||||||
@Query('query') filterBySearchQuery?: string,
|
|
||||||
@Query('range') dateRange: DateRange = 'max',
|
|
||||||
@Query('tags') filterByTags?: string
|
|
||||||
): Promise<PortfolioPositions> {
|
|
||||||
const filters = this.apiService.buildFiltersFromQueryParams({
|
|
||||||
filterByAccounts,
|
|
||||||
filterByAssetClasses,
|
|
||||||
filterBySearchQuery,
|
|
||||||
filterByTags
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.portfolioService.getPositions({
|
|
||||||
dateRange,
|
|
||||||
filters,
|
|
||||||
impersonationId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('public/:accessId')
|
@Get('public/:accessId')
|
||||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||||
public async getPublic(
|
public async getPublic(
|
||||||
|
@ -27,7 +27,7 @@ describe('PortfolioService', () => {
|
|||||||
portfolioService
|
portfolioService
|
||||||
.getAnnualizedPerformancePercent({
|
.getAnnualizedPerformancePercent({
|
||||||
daysInMarket: NaN, // differenceInDays of date-fns returns NaN for the same day
|
daysInMarket: NaN, // differenceInDays of date-fns returns NaN for the same day
|
||||||
netPerformancePercent: new Big(0)
|
netPerformancePercentage: new Big(0)
|
||||||
})
|
})
|
||||||
.toNumber()
|
.toNumber()
|
||||||
).toEqual(0);
|
).toEqual(0);
|
||||||
@ -36,7 +36,7 @@ describe('PortfolioService', () => {
|
|||||||
portfolioService
|
portfolioService
|
||||||
.getAnnualizedPerformancePercent({
|
.getAnnualizedPerformancePercent({
|
||||||
daysInMarket: 0,
|
daysInMarket: 0,
|
||||||
netPerformancePercent: new Big(0)
|
netPerformancePercentage: new Big(0)
|
||||||
})
|
})
|
||||||
.toNumber()
|
.toNumber()
|
||||||
).toEqual(0);
|
).toEqual(0);
|
||||||
@ -48,7 +48,7 @@ describe('PortfolioService', () => {
|
|||||||
portfolioService
|
portfolioService
|
||||||
.getAnnualizedPerformancePercent({
|
.getAnnualizedPerformancePercent({
|
||||||
daysInMarket: 65, // < 1 year
|
daysInMarket: 65, // < 1 year
|
||||||
netPerformancePercent: new Big(0.1025)
|
netPerformancePercentage: new Big(0.1025)
|
||||||
})
|
})
|
||||||
.toNumber()
|
.toNumber()
|
||||||
).toBeCloseTo(0.729705);
|
).toBeCloseTo(0.729705);
|
||||||
@ -57,7 +57,7 @@ describe('PortfolioService', () => {
|
|||||||
portfolioService
|
portfolioService
|
||||||
.getAnnualizedPerformancePercent({
|
.getAnnualizedPerformancePercent({
|
||||||
daysInMarket: 365, // 1 year
|
daysInMarket: 365, // 1 year
|
||||||
netPerformancePercent: new Big(0.05)
|
netPerformancePercentage: new Big(0.05)
|
||||||
})
|
})
|
||||||
.toNumber()
|
.toNumber()
|
||||||
).toBeCloseTo(0.05);
|
).toBeCloseTo(0.05);
|
||||||
@ -69,7 +69,7 @@ describe('PortfolioService', () => {
|
|||||||
portfolioService
|
portfolioService
|
||||||
.getAnnualizedPerformancePercent({
|
.getAnnualizedPerformancePercent({
|
||||||
daysInMarket: 575, // > 1 year
|
daysInMarket: 575, // > 1 year
|
||||||
netPerformancePercent: new Big(0.2374)
|
netPerformancePercentage: new Big(0.2374)
|
||||||
})
|
})
|
||||||
.toNumber()
|
.toNumber()
|
||||||
).toBeCloseTo(0.145);
|
).toBeCloseTo(0.145);
|
||||||
|
@ -208,16 +208,16 @@ export class PortfolioService {
|
|||||||
|
|
||||||
public getAnnualizedPerformancePercent({
|
public getAnnualizedPerformancePercent({
|
||||||
daysInMarket,
|
daysInMarket,
|
||||||
netPerformancePercent
|
netPerformancePercentage
|
||||||
}: {
|
}: {
|
||||||
daysInMarket: number;
|
daysInMarket: number;
|
||||||
netPerformancePercent: Big;
|
netPerformancePercentage: Big;
|
||||||
}): Big {
|
}): Big {
|
||||||
if (isNumber(daysInMarket) && daysInMarket > 0) {
|
if (isNumber(daysInMarket) && daysInMarket > 0) {
|
||||||
const exponent = new Big(365).div(daysInMarket).toNumber();
|
const exponent = new Big(365).div(daysInMarket).toNumber();
|
||||||
|
|
||||||
return new Big(
|
return new Big(
|
||||||
Math.pow(netPerformancePercent.plus(1).toNumber(), exponent)
|
Math.pow(netPerformancePercentage.plus(1).toNumber(), exponent)
|
||||||
).minus(1);
|
).minus(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -277,9 +277,11 @@ export class PortfolioService {
|
|||||||
|
|
||||||
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
||||||
activities,
|
activities,
|
||||||
|
dateRange,
|
||||||
userId,
|
userId,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: this.request.user.Settings.settings.baseCurrency,
|
currency: this.request.user.Settings.settings.baseCurrency,
|
||||||
|
hasFilters: filters?.length > 0,
|
||||||
isExperimentalFeatures:
|
isExperimentalFeatures:
|
||||||
this.request.user.Settings.settings.isExperimentalFeatures
|
this.request.user.Settings.settings.isExperimentalFeatures
|
||||||
});
|
});
|
||||||
@ -358,8 +360,9 @@ export class PortfolioService {
|
|||||||
userId,
|
userId,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: userCurrency,
|
currency: userCurrency,
|
||||||
|
hasFilters: true, // disable cache
|
||||||
isExperimentalFeatures:
|
isExperimentalFeatures:
|
||||||
this.request.user.Settings.settings.isExperimentalFeatures
|
this.request.user?.Settings.settings.isExperimentalFeatures
|
||||||
});
|
});
|
||||||
|
|
||||||
const { currentValueInBaseCurrency, hasErrors, positions } =
|
const { currentValueInBaseCurrency, hasErrors, positions } =
|
||||||
@ -660,6 +663,7 @@ export class PortfolioService {
|
|||||||
}),
|
}),
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: userCurrency,
|
currency: userCurrency,
|
||||||
|
hasFilters: true,
|
||||||
isExperimentalFeatures:
|
isExperimentalFeatures:
|
||||||
this.request.user.Settings.settings.isExperimentalFeatures
|
this.request.user.Settings.settings.isExperimentalFeatures
|
||||||
});
|
});
|
||||||
@ -700,17 +704,21 @@ export class PortfolioService {
|
|||||||
|
|
||||||
const dividendYieldPercent = this.getAnnualizedPerformancePercent({
|
const dividendYieldPercent = this.getAnnualizedPerformancePercent({
|
||||||
daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)),
|
daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)),
|
||||||
netPerformancePercent: dividendInBaseCurrency.div(
|
netPerformancePercentage: timeWeightedInvestment.eq(0)
|
||||||
timeWeightedInvestment
|
? new Big(0)
|
||||||
)
|
: dividendInBaseCurrency.div(timeWeightedInvestment)
|
||||||
});
|
});
|
||||||
|
|
||||||
const dividendYieldPercentWithCurrencyEffect =
|
const dividendYieldPercentWithCurrencyEffect =
|
||||||
this.getAnnualizedPerformancePercent({
|
this.getAnnualizedPerformancePercent({
|
||||||
daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)),
|
daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)),
|
||||||
netPerformancePercent: dividendInBaseCurrency.div(
|
netPerformancePercentage: timeWeightedInvestmentWithCurrencyEffect.eq(
|
||||||
timeWeightedInvestmentWithCurrencyEffect
|
0
|
||||||
)
|
)
|
||||||
|
? new Big(0)
|
||||||
|
: dividendInBaseCurrency.div(
|
||||||
|
timeWeightedInvestmentWithCurrencyEffect
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
const historicalData = await this.dataProviderService.getHistorical(
|
const historicalData = await this.dataProviderService.getHistorical(
|
||||||
@ -931,6 +939,7 @@ export class PortfolioService {
|
|||||||
userId,
|
userId,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: this.request.user.Settings.settings.baseCurrency,
|
currency: this.request.user.Settings.settings.baseCurrency,
|
||||||
|
hasFilters: filters?.length > 0,
|
||||||
isExperimentalFeatures:
|
isExperimentalFeatures:
|
||||||
this.request.user.Settings.settings.isExperimentalFeatures
|
this.request.user.Settings.settings.isExperimentalFeatures
|
||||||
});
|
});
|
||||||
@ -1085,7 +1094,7 @@ export class PortfolioService {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const { endDate, startDate } = getInterval(dateRange);
|
const { endDate } = getInterval(dateRange);
|
||||||
|
|
||||||
const { activities } = await this.orderService.getOrders({
|
const { activities } = await this.orderService.getOrders({
|
||||||
endDate,
|
endDate,
|
||||||
@ -1101,16 +1110,16 @@ export class PortfolioService {
|
|||||||
firstOrderDate: undefined,
|
firstOrderDate: undefined,
|
||||||
hasErrors: false,
|
hasErrors: false,
|
||||||
performance: {
|
performance: {
|
||||||
currentGrossPerformance: 0,
|
|
||||||
currentGrossPerformancePercent: 0,
|
|
||||||
currentGrossPerformancePercentWithCurrencyEffect: 0,
|
|
||||||
currentGrossPerformanceWithCurrencyEffect: 0,
|
|
||||||
currentNetPerformance: 0,
|
|
||||||
currentNetPerformancePercent: 0,
|
|
||||||
currentNetPerformancePercentWithCurrencyEffect: 0,
|
|
||||||
currentNetPerformanceWithCurrencyEffect: 0,
|
|
||||||
currentNetWorth: 0,
|
currentNetWorth: 0,
|
||||||
currentValue: 0,
|
currentValueInBaseCurrency: 0,
|
||||||
|
grossPerformance: 0,
|
||||||
|
grossPerformancePercentage: 0,
|
||||||
|
grossPerformancePercentageWithCurrencyEffect: 0,
|
||||||
|
grossPerformanceWithCurrencyEffect: 0,
|
||||||
|
netPerformance: 0,
|
||||||
|
netPerformancePercentage: 0,
|
||||||
|
netPerformancePercentageWithCurrencyEffect: 0,
|
||||||
|
netPerformanceWithCurrencyEffect: 0,
|
||||||
totalInvestment: 0
|
totalInvestment: 0
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -1123,6 +1132,7 @@ export class PortfolioService {
|
|||||||
userId,
|
userId,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: userCurrency,
|
currency: userCurrency,
|
||||||
|
hasFilters: filters?.length > 0,
|
||||||
isExperimentalFeatures:
|
isExperimentalFeatures:
|
||||||
this.request.user.Settings.settings.isExperimentalFeatures
|
this.request.user.Settings.settings.isExperimentalFeatures
|
||||||
});
|
});
|
||||||
@ -1144,9 +1154,9 @@ export class PortfolioService {
|
|||||||
|
|
||||||
let currentNetPerformance = netPerformance;
|
let currentNetPerformance = netPerformance;
|
||||||
|
|
||||||
let currentNetPerformancePercent = netPerformancePercentage;
|
let currentNetPerformancePercentage = netPerformancePercentage;
|
||||||
|
|
||||||
let currentNetPerformancePercentWithCurrencyEffect =
|
let currentNetPerformancePercentageWithCurrencyEffect =
|
||||||
netPerformancePercentageWithCurrencyEffect;
|
netPerformancePercentageWithCurrencyEffect;
|
||||||
|
|
||||||
let currentNetPerformanceWithCurrencyEffect =
|
let currentNetPerformanceWithCurrencyEffect =
|
||||||
@ -1165,11 +1175,11 @@ export class PortfolioService {
|
|||||||
if (itemOfToday) {
|
if (itemOfToday) {
|
||||||
currentNetPerformance = new Big(itemOfToday.netPerformance);
|
currentNetPerformance = new Big(itemOfToday.netPerformance);
|
||||||
|
|
||||||
currentNetPerformancePercent = new Big(
|
currentNetPerformancePercentage = new Big(
|
||||||
itemOfToday.netPerformanceInPercentage
|
itemOfToday.netPerformanceInPercentage
|
||||||
).div(100);
|
).div(100);
|
||||||
|
|
||||||
currentNetPerformancePercentWithCurrencyEffect = new Big(
|
currentNetPerformancePercentageWithCurrencyEffect = new Big(
|
||||||
itemOfToday.netPerformanceInPercentageWithCurrencyEffect
|
itemOfToday.netPerformanceInPercentageWithCurrencyEffect
|
||||||
).div(100);
|
).div(100);
|
||||||
|
|
||||||
@ -1187,19 +1197,19 @@ export class PortfolioService {
|
|||||||
firstOrderDate: parseDate(items[0]?.date),
|
firstOrderDate: parseDate(items[0]?.date),
|
||||||
performance: {
|
performance: {
|
||||||
currentNetWorth,
|
currentNetWorth,
|
||||||
currentGrossPerformance: grossPerformance.toNumber(),
|
currentValueInBaseCurrency: currentValueInBaseCurrency.toNumber(),
|
||||||
currentGrossPerformancePercent: grossPerformancePercentage.toNumber(),
|
grossPerformance: grossPerformance.toNumber(),
|
||||||
currentGrossPerformancePercentWithCurrencyEffect:
|
grossPerformancePercentage: grossPerformancePercentage.toNumber(),
|
||||||
|
grossPerformancePercentageWithCurrencyEffect:
|
||||||
grossPerformancePercentageWithCurrencyEffect.toNumber(),
|
grossPerformancePercentageWithCurrencyEffect.toNumber(),
|
||||||
currentGrossPerformanceWithCurrencyEffect:
|
grossPerformanceWithCurrencyEffect:
|
||||||
grossPerformanceWithCurrencyEffect.toNumber(),
|
grossPerformanceWithCurrencyEffect.toNumber(),
|
||||||
currentNetPerformance: currentNetPerformance.toNumber(),
|
netPerformance: currentNetPerformance.toNumber(),
|
||||||
currentNetPerformancePercent: currentNetPerformancePercent.toNumber(),
|
netPerformancePercentage: currentNetPerformancePercentage.toNumber(),
|
||||||
currentNetPerformancePercentWithCurrencyEffect:
|
netPerformancePercentageWithCurrencyEffect:
|
||||||
currentNetPerformancePercentWithCurrencyEffect.toNumber(),
|
currentNetPerformancePercentageWithCurrencyEffect.toNumber(),
|
||||||
currentNetPerformanceWithCurrencyEffect:
|
netPerformanceWithCurrencyEffect:
|
||||||
currentNetPerformanceWithCurrencyEffect.toNumber(),
|
currentNetPerformanceWithCurrencyEffect.toNumber(),
|
||||||
currentValue: currentValueInBaseCurrency.toNumber(),
|
|
||||||
totalInvestment: totalInvestment.toNumber()
|
totalInvestment: totalInvestment.toNumber()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -1220,6 +1230,7 @@ export class PortfolioService {
|
|||||||
userId,
|
userId,
|
||||||
calculationType: PerformanceCalculationType.TWR,
|
calculationType: PerformanceCalculationType.TWR,
|
||||||
currency: this.request.user.Settings.settings.baseCurrency,
|
currency: this.request.user.Settings.settings.baseCurrency,
|
||||||
|
hasFilters: false,
|
||||||
isExperimentalFeatures:
|
isExperimentalFeatures:
|
||||||
this.request.user.Settings.settings.isExperimentalFeatures
|
this.request.user.Settings.settings.isExperimentalFeatures
|
||||||
});
|
});
|
||||||
@ -1595,11 +1606,6 @@ export class PortfolioService {
|
|||||||
userId = await this.getUserId(impersonationId, userId);
|
userId = await this.getUserId(impersonationId, userId);
|
||||||
const user = await this.userService.user({ id: userId });
|
const user = await this.userService.user({ id: userId });
|
||||||
|
|
||||||
const performanceInformation = await this.getPerformance({
|
|
||||||
impersonationId,
|
|
||||||
userId
|
|
||||||
});
|
|
||||||
|
|
||||||
const { activities } = await this.orderService.getOrders({
|
const { activities } = await this.orderService.getOrders({
|
||||||
userCurrency,
|
userCurrency,
|
||||||
userId,
|
userId,
|
||||||
@ -1617,6 +1623,19 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
currentValueInBaseCurrency,
|
||||||
|
grossPerformance,
|
||||||
|
grossPerformancePercentage,
|
||||||
|
grossPerformancePercentageWithCurrencyEffect,
|
||||||
|
grossPerformanceWithCurrencyEffect,
|
||||||
|
netPerformance,
|
||||||
|
netPerformancePercentage,
|
||||||
|
netPerformancePercentageWithCurrencyEffect,
|
||||||
|
netPerformanceWithCurrencyEffect,
|
||||||
|
totalInvestment
|
||||||
|
} = await portfolioCalculator.getSnapshot();
|
||||||
|
|
||||||
const dividendInBaseCurrency =
|
const dividendInBaseCurrency =
|
||||||
await portfolioCalculator.getDividendInBaseCurrency();
|
await portfolioCalculator.getDividendInBaseCurrency();
|
||||||
|
|
||||||
@ -1685,7 +1704,7 @@ export class PortfolioService {
|
|||||||
.toNumber();
|
.toNumber();
|
||||||
|
|
||||||
const netWorth = new Big(balanceInBaseCurrency)
|
const netWorth = new Big(balanceInBaseCurrency)
|
||||||
.plus(performanceInformation.performance.currentValue)
|
.plus(currentValueInBaseCurrency)
|
||||||
.plus(valuables)
|
.plus(valuables)
|
||||||
.plus(excludedAccountsAndActivities)
|
.plus(excludedAccountsAndActivities)
|
||||||
.minus(liabilities)
|
.minus(liabilities)
|
||||||
@ -1695,21 +1714,18 @@ export class PortfolioService {
|
|||||||
|
|
||||||
const annualizedPerformancePercent = this.getAnnualizedPerformancePercent({
|
const annualizedPerformancePercent = this.getAnnualizedPerformancePercent({
|
||||||
daysInMarket,
|
daysInMarket,
|
||||||
netPerformancePercent: new Big(
|
netPerformancePercentage: new Big(netPerformancePercentage)
|
||||||
performanceInformation.performance.currentNetPerformancePercent
|
|
||||||
)
|
|
||||||
})?.toNumber();
|
})?.toNumber();
|
||||||
|
|
||||||
const annualizedPerformancePercentWithCurrencyEffect =
|
const annualizedPerformancePercentWithCurrencyEffect =
|
||||||
this.getAnnualizedPerformancePercent({
|
this.getAnnualizedPerformancePercent({
|
||||||
daysInMarket,
|
daysInMarket,
|
||||||
netPerformancePercent: new Big(
|
netPerformancePercentage: new Big(
|
||||||
performanceInformation.performance.currentNetPerformancePercentWithCurrencyEffect
|
netPerformancePercentageWithCurrencyEffect
|
||||||
)
|
)
|
||||||
})?.toNumber();
|
})?.toNumber();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...performanceInformation.performance,
|
|
||||||
annualizedPerformancePercent,
|
annualizedPerformancePercent,
|
||||||
annualizedPerformancePercentWithCurrencyEffect,
|
annualizedPerformancePercentWithCurrencyEffect,
|
||||||
cash,
|
cash,
|
||||||
@ -1718,6 +1734,7 @@ export class PortfolioService {
|
|||||||
totalBuy,
|
totalBuy,
|
||||||
totalSell,
|
totalSell,
|
||||||
committedFunds: committedFunds.toNumber(),
|
committedFunds: committedFunds.toNumber(),
|
||||||
|
currentValueInBaseCurrency: currentValueInBaseCurrency.toNumber(),
|
||||||
dividendInBaseCurrency: dividendInBaseCurrency.toNumber(),
|
dividendInBaseCurrency: dividendInBaseCurrency.toNumber(),
|
||||||
emergencyFund: {
|
emergencyFund: {
|
||||||
assets: emergencyFundPositionsValueInBaseCurrency,
|
assets: emergencyFundPositionsValueInBaseCurrency,
|
||||||
@ -1731,15 +1748,28 @@ export class PortfolioService {
|
|||||||
filteredValueInPercentage: netWorth
|
filteredValueInPercentage: netWorth
|
||||||
? filteredValueInBaseCurrency.div(netWorth).toNumber()
|
? filteredValueInBaseCurrency.div(netWorth).toNumber()
|
||||||
: undefined,
|
: undefined,
|
||||||
fireWealth: new Big(performanceInformation.performance.currentValue)
|
fireWealth: new Big(currentValueInBaseCurrency)
|
||||||
.minus(emergencyFundPositionsValueInBaseCurrency)
|
.minus(emergencyFundPositionsValueInBaseCurrency)
|
||||||
.toNumber(),
|
.toNumber(),
|
||||||
|
grossPerformance: grossPerformance.toNumber(),
|
||||||
|
grossPerformancePercentage: grossPerformancePercentage.toNumber(),
|
||||||
|
grossPerformancePercentageWithCurrencyEffect:
|
||||||
|
grossPerformancePercentageWithCurrencyEffect.toNumber(),
|
||||||
|
grossPerformanceWithCurrencyEffect:
|
||||||
|
grossPerformanceWithCurrencyEffect.toNumber(),
|
||||||
interest: interest.toNumber(),
|
interest: interest.toNumber(),
|
||||||
items: valuables.toNumber(),
|
items: valuables.toNumber(),
|
||||||
liabilities: liabilities.toNumber(),
|
liabilities: liabilities.toNumber(),
|
||||||
|
netPerformance: netPerformance.toNumber(),
|
||||||
|
netPerformancePercentage: netPerformancePercentage.toNumber(),
|
||||||
|
netPerformancePercentageWithCurrencyEffect:
|
||||||
|
netPerformancePercentageWithCurrencyEffect.toNumber(),
|
||||||
|
netPerformanceWithCurrencyEffect:
|
||||||
|
netPerformanceWithCurrencyEffect.toNumber(),
|
||||||
ordersCount: activities.filter(({ type }) => {
|
ordersCount: activities.filter(({ type }) => {
|
||||||
return type === 'BUY' || type === 'SELL';
|
return ['BUY', 'SELL'].includes(type);
|
||||||
}).length,
|
}).length,
|
||||||
|
totalInvestment: totalInvestment.toNumber(),
|
||||||
totalValueInBaseCurrency: netWorth
|
totalValueInBaseCurrency: netWorth
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ export class RedisCacheService {
|
|||||||
return this.cache.get(key);
|
return this.cache.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPortfolioSnapshotKey(userId: string) {
|
public getPortfolioSnapshotKey({ userId }: { userId: string }) {
|
||||||
return `portfolio-snapshot-${userId}`;
|
return `portfolio-snapshot-${userId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,11 +2,7 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorat
|
|||||||
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
||||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||||
import { User, UserSettings } from '@ghostfolio/common/interfaces';
|
import { User, UserSettings } from '@ghostfolio/common/interfaces';
|
||||||
import {
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
hasPermission,
|
|
||||||
hasRole,
|
|
||||||
permissions
|
|
||||||
} from '@ghostfolio/common/permissions';
|
|
||||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -63,13 +59,6 @@ export class UserController {
|
|||||||
public async getUser(
|
public async getUser(
|
||||||
@Headers('accept-language') acceptLanguage: string
|
@Headers('accept-language') acceptLanguage: string
|
||||||
): Promise<User> {
|
): Promise<User> {
|
||||||
if (hasRole(this.request.user, 'INACTIVE')) {
|
|
||||||
throw new HttpException(
|
|
||||||
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
|
|
||||||
StatusCodes.TOO_MANY_REQUESTS
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.userService.getUser(
|
return this.userService.getUser(
|
||||||
this.request.user,
|
this.request.user,
|
||||||
acceptLanguage?.split(',')?.[0]
|
acceptLanguage?.split(',')?.[0]
|
||||||
|
@ -12,12 +12,14 @@ export class PortfolioChangedListener {
|
|||||||
@OnEvent(PortfolioChangedEvent.getName())
|
@OnEvent(PortfolioChangedEvent.getName())
|
||||||
handlePortfolioChangedEvent(event: PortfolioChangedEvent) {
|
handlePortfolioChangedEvent(event: PortfolioChangedEvent) {
|
||||||
Logger.log(
|
Logger.log(
|
||||||
`Portfolio of user with id ${event.getUserId()} has changed`,
|
`Portfolio of user '${event.getUserId()}' has changed`,
|
||||||
'PortfolioChangedListener'
|
'PortfolioChangedListener'
|
||||||
);
|
);
|
||||||
|
|
||||||
this.redisCacheService.remove(
|
this.redisCacheService.remove(
|
||||||
this.redisCacheService.getPortfolioSnapshotKey(event.getUserId())
|
this.redisCacheService.getPortfolioSnapshotKey({
|
||||||
|
userId: event.getUserId()
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -158,11 +158,9 @@
|
|||||||
<li>
|
<li>
|
||||||
<a href="../pt" title="Ghostfolio in Português">Português</a>
|
<a href="../pt" title="Ghostfolio in Português">Português</a>
|
||||||
</li>
|
</li>
|
||||||
<!--
|
<li>
|
||||||
<li>
|
<a href="../tr" title="Ghostfolio in Türkçe">Türkçe</a>
|
||||||
<a href="../tr" title="Ghostfolio in Türkçe">Türkçe</a>
|
</li>
|
||||||
</li>
|
|
||||||
-->
|
|
||||||
<!--
|
<!--
|
||||||
<li>
|
<li>
|
||||||
<a href="../zh" title="Ghostfolio in Chinese">Chinese</a>
|
<a href="../zh" title="Ghostfolio in Chinese">Chinese</a>
|
||||||
|
@ -13,13 +13,23 @@ import {
|
|||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit
|
OnInit
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
import { NavigationEnd, PRIMARY_OUTLET, Router } from '@angular/router';
|
import {
|
||||||
|
ActivatedRoute,
|
||||||
|
NavigationEnd,
|
||||||
|
PRIMARY_OUTLET,
|
||||||
|
Router
|
||||||
|
} from '@angular/router';
|
||||||
|
import { DataSource } from '@prisma/client';
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { filter, takeUntil } from 'rxjs/operators';
|
import { filter, takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { PositionDetailDialogParams } from './components/position-detail-dialog/interfaces/interfaces';
|
||||||
|
import { PositionDetailDialog } from './components/position-detail-dialog/position-detail-dialog.component';
|
||||||
import { DataService } from './services/data.service';
|
import { DataService } from './services/data.service';
|
||||||
|
import { ImpersonationStorageService } from './services/impersonation-storage.service';
|
||||||
import { TokenStorageService } from './services/token-storage.service';
|
import { TokenStorageService } from './services/token-storage.service';
|
||||||
import { UserService } from './services/user/user.service';
|
import { UserService } from './services/user/user.service';
|
||||||
|
|
||||||
@ -38,6 +48,7 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
public currentRoute: string;
|
public currentRoute: string;
|
||||||
public currentYear = new Date().getFullYear();
|
public currentYear = new Date().getFullYear();
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
|
public hasImpersonationId: boolean;
|
||||||
public hasInfoMessage: boolean;
|
public hasInfoMessage: boolean;
|
||||||
public hasPermissionForStatistics: boolean;
|
public hasPermissionForStatistics: boolean;
|
||||||
public hasPermissionForSubscription: boolean;
|
public hasPermissionForSubscription: boolean;
|
||||||
@ -67,7 +78,10 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
private deviceService: DeviceDetectorService,
|
private deviceService: DeviceDetectorService,
|
||||||
|
private dialog: MatDialog,
|
||||||
@Inject(DOCUMENT) private document: Document,
|
@Inject(DOCUMENT) private document: Document,
|
||||||
|
private impersonationStorageService: ImpersonationStorageService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private title: Title,
|
private title: Title,
|
||||||
private tokenStorageService: TokenStorageService,
|
private tokenStorageService: TokenStorageService,
|
||||||
@ -75,6 +89,21 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
) {
|
) {
|
||||||
this.initializeTheme();
|
this.initializeTheme();
|
||||||
this.user = undefined;
|
this.user = undefined;
|
||||||
|
|
||||||
|
this.route.queryParams
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((params) => {
|
||||||
|
if (
|
||||||
|
params['dataSource'] &&
|
||||||
|
params['holdingDetailDialog'] &&
|
||||||
|
params['symbol']
|
||||||
|
) {
|
||||||
|
this.openHoldingDetailDialog({
|
||||||
|
dataSource: params['dataSource'],
|
||||||
|
symbol: params['symbol']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnInit() {
|
public ngOnInit() {
|
||||||
@ -96,6 +125,13 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
permissions.enableFearAndGreedIndex
|
permissions.enableFearAndGreedIndex
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.impersonationStorageService
|
||||||
|
.onChangeHasImpersonation()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((impersonationId) => {
|
||||||
|
this.hasImpersonationId = !!impersonationId;
|
||||||
|
});
|
||||||
|
|
||||||
this.router.events
|
this.router.events
|
||||||
.pipe(filter((event) => event instanceof NavigationEnd))
|
.pipe(filter((event) => event instanceof NavigationEnd))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
@ -197,6 +233,55 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private openHoldingDetailDialog({
|
||||||
|
dataSource,
|
||||||
|
symbol
|
||||||
|
}: {
|
||||||
|
dataSource: DataSource;
|
||||||
|
symbol: string;
|
||||||
|
}) {
|
||||||
|
this.userService
|
||||||
|
.get()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((user) => {
|
||||||
|
this.user = user;
|
||||||
|
|
||||||
|
const dialogRef = this.dialog.open(PositionDetailDialog, {
|
||||||
|
autoFocus: false,
|
||||||
|
data: <PositionDetailDialogParams>{
|
||||||
|
dataSource,
|
||||||
|
symbol,
|
||||||
|
baseCurrency: this.user?.settings?.baseCurrency,
|
||||||
|
colorScheme: this.user?.settings?.colorScheme,
|
||||||
|
deviceType: this.deviceType,
|
||||||
|
hasImpersonationId: this.hasImpersonationId,
|
||||||
|
hasPermissionToReportDataGlitch: hasPermission(
|
||||||
|
this.user?.permissions,
|
||||||
|
permissions.reportDataGlitch
|
||||||
|
),
|
||||||
|
locale: this.user?.settings?.locale
|
||||||
|
},
|
||||||
|
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||||
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef
|
||||||
|
.afterClosed()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.router.navigate([], {
|
||||||
|
queryParams: {
|
||||||
|
dataSource: null,
|
||||||
|
holdingDetailDialog: null,
|
||||||
|
symbol: null
|
||||||
|
},
|
||||||
|
queryParamsHandling: 'merge',
|
||||||
|
relativeTo: this.route
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private toggleTheme(isDarkTheme: boolean) {
|
private toggleTheme(isDarkTheme: boolean) {
|
||||||
const themeColor = getCssVariable(
|
const themeColor = getCssVariable(
|
||||||
isDarkTheme ? '--dark-background' : '--light-background'
|
isDarkTheme ? '--dark-background' : '--light-background'
|
||||||
|
@ -94,6 +94,7 @@
|
|||||||
[dataSource]="dataSource"
|
[dataSource]="dataSource"
|
||||||
[deviceType]="data.deviceType"
|
[deviceType]="data.deviceType"
|
||||||
[hasPermissionToCreateActivity]="false"
|
[hasPermissionToCreateActivity]="false"
|
||||||
|
[hasPermissionToDeleteActivity]="false"
|
||||||
[hasPermissionToExportActivities]="
|
[hasPermissionToExportActivities]="
|
||||||
!data.hasImpersonationId && !user.settings.isRestrictedView
|
!data.hasImpersonationId && !user.settings.isRestrictedView
|
||||||
"
|
"
|
||||||
|
@ -38,6 +38,7 @@ import { CreateAssetProfileDialogParams } from './create-asset-profile-dialog/in
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
host: { class: 'has-fab' },
|
||||||
selector: 'gf-admin-market-data',
|
selector: 'gf-admin-market-data',
|
||||||
styleUrls: ['./admin-market-data.scss'],
|
styleUrls: ['./admin-market-data.scss'],
|
||||||
templateUrl: './admin-market-data.html'
|
templateUrl: './admin-market-data.html'
|
||||||
|
@ -3,6 +3,7 @@ import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-dat
|
|||||||
import { AdminMarketDataService } from '@ghostfolio/client/components/admin-market-data/admin-market-data.service';
|
import { AdminMarketDataService } from '@ghostfolio/client/components/admin-market-data/admin-market-data.service';
|
||||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
|
import { validateObjectForForm } from '@ghostfolio/client/util/form.util';
|
||||||
import { ghostfolioScraperApiSymbolPrefix } from '@ghostfolio/common/config';
|
import { ghostfolioScraperApiSymbolPrefix } from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||||
import {
|
import {
|
||||||
@ -258,7 +259,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onSubmit() {
|
public async onSubmit() {
|
||||||
let countries = [];
|
let countries = [];
|
||||||
let scraperConfiguration = {};
|
let scraperConfiguration = {};
|
||||||
let sectors = [];
|
let sectors = [];
|
||||||
@ -299,6 +300,17 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
|||||||
url: this.assetProfileForm.get('url').value || null
|
url: this.assetProfileForm.get('url').value || null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await validateObjectForForm({
|
||||||
|
classDto: UpdateAssetProfileDto,
|
||||||
|
form: this.assetProfileForm,
|
||||||
|
object: assetProfileData
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.adminService
|
this.adminService
|
||||||
.patchAssetProfile({
|
.patchAssetProfile({
|
||||||
...assetProfileData,
|
...assetProfileData,
|
||||||
|
@ -143,9 +143,7 @@ export class AdminPlatformComponent implements OnInit, OnDestroy {
|
|||||||
dialogRef
|
dialogRef
|
||||||
.afterClosed()
|
.afterClosed()
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((data) => {
|
.subscribe((platform: CreatePlatformDto | null) => {
|
||||||
const platform: CreatePlatformDto = data?.platform;
|
|
||||||
|
|
||||||
if (platform) {
|
if (platform) {
|
||||||
this.adminService
|
this.adminService
|
||||||
.postPlatform(platform)
|
.postPlatform(platform)
|
||||||
@ -182,9 +180,7 @@ export class AdminPlatformComponent implements OnInit, OnDestroy {
|
|||||||
dialogRef
|
dialogRef
|
||||||
.afterClosed()
|
.afterClosed()
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((data) => {
|
.subscribe((platform: UpdatePlatformDto | null) => {
|
||||||
const platform: UpdatePlatformDto = data?.platform;
|
|
||||||
|
|
||||||
if (platform) {
|
if (platform) {
|
||||||
this.adminService
|
this.adminService
|
||||||
.putPlatform(platform)
|
.putPlatform(platform)
|
||||||
|
@ -1,4 +1,14 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
|
import { CreatePlatformDto } from '@ghostfolio/api/app/platform/create-platform.dto';
|
||||||
|
import { UpdatePlatformDto } from '@ghostfolio/api/app/platform/update-platform.dto';
|
||||||
|
import { validateObjectForForm } from '@ghostfolio/client/util/form.util';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
Inject,
|
||||||
|
OnDestroy
|
||||||
|
} from '@angular/core';
|
||||||
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
@ -11,18 +21,54 @@ import { CreateOrUpdatePlatformDialogParams } from './interfaces/interfaces';
|
|||||||
styleUrls: ['./create-or-update-platform-dialog.scss'],
|
styleUrls: ['./create-or-update-platform-dialog.scss'],
|
||||||
templateUrl: 'create-or-update-platform-dialog.html'
|
templateUrl: 'create-or-update-platform-dialog.html'
|
||||||
})
|
})
|
||||||
export class CreateOrUpdatePlatformDialog {
|
export class CreateOrUpdatePlatformDialog implements OnDestroy {
|
||||||
|
public platformForm: FormGroup;
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdatePlatformDialogParams,
|
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdatePlatformDialogParams,
|
||||||
public dialogRef: MatDialogRef<CreateOrUpdatePlatformDialog>
|
public dialogRef: MatDialogRef<CreateOrUpdatePlatformDialog>,
|
||||||
) {}
|
private formBuilder: FormBuilder
|
||||||
|
) {
|
||||||
|
this.platformForm = this.formBuilder.group({
|
||||||
|
name: [this.data.platform.name, Validators.required],
|
||||||
|
url: [this.data.platform.url, Validators.required]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public onCancel() {
|
public onCancel() {
|
||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async onSubmit() {
|
||||||
|
try {
|
||||||
|
const platform: CreatePlatformDto | UpdatePlatformDto = {
|
||||||
|
name: this.platformForm.get('name')?.value,
|
||||||
|
url: this.platformForm.get('url')?.value
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.data.platform.id) {
|
||||||
|
(platform as UpdatePlatformDto).id = this.data.platform.id;
|
||||||
|
await validateObjectForForm({
|
||||||
|
classDto: UpdatePlatformDto,
|
||||||
|
form: this.platformForm,
|
||||||
|
object: platform
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await validateObjectForForm({
|
||||||
|
classDto: CreatePlatformDto,
|
||||||
|
form: this.platformForm,
|
||||||
|
object: platform
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dialogRef.close(platform);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
this.unsubscribeSubject.next();
|
this.unsubscribeSubject.next();
|
||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
|
@ -1,17 +1,30 @@
|
|||||||
<form #addPlatformForm="ngForm" class="d-flex flex-column h-100">
|
<form
|
||||||
|
class="d-flex flex-column h-100"
|
||||||
|
[formGroup]="platformForm"
|
||||||
|
(keyup.enter)="platformForm.valid && onSubmit()"
|
||||||
|
(ngSubmit)="onSubmit()"
|
||||||
|
>
|
||||||
<h1 *ngIf="data.platform.id" i18n mat-dialog-title>Update platform</h1>
|
<h1 *ngIf="data.platform.id" i18n mat-dialog-title>Update platform</h1>
|
||||||
<h1 *ngIf="!data.platform.id" i18n mat-dialog-title>Add platform</h1>
|
<h1 *ngIf="!data.platform.id" i18n mat-dialog-title>Add platform</h1>
|
||||||
<div class="flex-grow-1 py-3" mat-dialog-content>
|
<div class="flex-grow-1 py-3" mat-dialog-content>
|
||||||
<div>
|
<div>
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
<mat-label i18n>Name</mat-label>
|
<mat-label i18n>Name</mat-label>
|
||||||
<input matInput name="name" required [(ngModel)]="data.platform.name" />
|
<input
|
||||||
|
formControlName="name"
|
||||||
|
matInput
|
||||||
|
(keydown.enter)="$event.stopPropagation()"
|
||||||
|
/>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
<mat-label i18n>Url</mat-label>
|
<mat-label i18n>Url</mat-label>
|
||||||
<input matInput name="url" required [(ngModel)]="data.platform.url" />
|
<input
|
||||||
|
formControlName="url"
|
||||||
|
matInput
|
||||||
|
(keydown.enter)="$event.stopPropagation()"
|
||||||
|
/>
|
||||||
@if (data.platform.url) {
|
@if (data.platform.url) {
|
||||||
<gf-asset-profile-icon
|
<gf-asset-profile-icon
|
||||||
class="mr-3"
|
class="mr-3"
|
||||||
@ -23,12 +36,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="justify-content-end" mat-dialog-actions>
|
<div class="justify-content-end" mat-dialog-actions>
|
||||||
<button i18n mat-button (click)="onCancel()">Cancel</button>
|
<button i18n mat-button type="button" (click)="onCancel()">Cancel</button>
|
||||||
<button
|
<button
|
||||||
color="primary"
|
color="primary"
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[disabled]="!addPlatformForm.form.valid"
|
type="submit"
|
||||||
[mat-dialog-close]="data"
|
[disabled]="!platformForm.valid"
|
||||||
>
|
>
|
||||||
<ng-container i18n>Save</ng-container>
|
<ng-container i18n>Save</ng-container>
|
||||||
</button>
|
</button>
|
||||||
|
@ -142,9 +142,7 @@ export class AdminTagComponent implements OnInit, OnDestroy {
|
|||||||
dialogRef
|
dialogRef
|
||||||
.afterClosed()
|
.afterClosed()
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((data) => {
|
.subscribe((tag: CreateTagDto | null) => {
|
||||||
const tag: CreateTagDto = data?.tag;
|
|
||||||
|
|
||||||
if (tag) {
|
if (tag) {
|
||||||
this.adminService
|
this.adminService
|
||||||
.postTag(tag)
|
.postTag(tag)
|
||||||
@ -180,9 +178,7 @@ export class AdminTagComponent implements OnInit, OnDestroy {
|
|||||||
dialogRef
|
dialogRef
|
||||||
.afterClosed()
|
.afterClosed()
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((data) => {
|
.subscribe((tag: UpdateTagDto | null) => {
|
||||||
const tag: UpdateTagDto = data?.tag;
|
|
||||||
|
|
||||||
if (tag) {
|
if (tag) {
|
||||||
this.adminService
|
this.adminService
|
||||||
.putTag(tag)
|
.putTag(tag)
|
||||||
|
@ -1,4 +1,14 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
|
import { CreateTagDto } from '@ghostfolio/api/app/tag/create-tag.dto';
|
||||||
|
import { UpdateTagDto } from '@ghostfolio/api/app/tag/update-tag.dto';
|
||||||
|
import { validateObjectForForm } from '@ghostfolio/client/util/form.util';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
Inject,
|
||||||
|
OnDestroy
|
||||||
|
} from '@angular/core';
|
||||||
|
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
@ -11,18 +21,52 @@ import { CreateOrUpdateTagDialogParams } from './interfaces/interfaces';
|
|||||||
styleUrls: ['./create-or-update-tag-dialog.scss'],
|
styleUrls: ['./create-or-update-tag-dialog.scss'],
|
||||||
templateUrl: 'create-or-update-tag-dialog.html'
|
templateUrl: 'create-or-update-tag-dialog.html'
|
||||||
})
|
})
|
||||||
export class CreateOrUpdateTagDialog {
|
export class CreateOrUpdateTagDialog implements OnDestroy {
|
||||||
|
public tagForm: FormGroup;
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateTagDialogParams,
|
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateTagDialogParams,
|
||||||
public dialogRef: MatDialogRef<CreateOrUpdateTagDialog>
|
public dialogRef: MatDialogRef<CreateOrUpdateTagDialog>,
|
||||||
) {}
|
private formBuilder: FormBuilder
|
||||||
|
) {
|
||||||
|
this.tagForm = this.formBuilder.group({
|
||||||
|
name: [this.data.tag.name]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public onCancel() {
|
public onCancel() {
|
||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async onSubmit() {
|
||||||
|
try {
|
||||||
|
const tag: CreateTagDto | UpdateTagDto = {
|
||||||
|
name: this.tagForm.get('name')?.value
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.data.tag.id) {
|
||||||
|
(tag as UpdateTagDto).id = this.data.tag.id;
|
||||||
|
await validateObjectForForm({
|
||||||
|
classDto: UpdateTagDto,
|
||||||
|
form: this.tagForm,
|
||||||
|
object: tag
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await validateObjectForForm({
|
||||||
|
classDto: CreateTagDto,
|
||||||
|
form: this.tagForm,
|
||||||
|
object: tag
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dialogRef.close(tag);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
this.unsubscribeSubject.next();
|
this.unsubscribeSubject.next();
|
||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
|
@ -1,21 +1,30 @@
|
|||||||
<form #addTagForm="ngForm" class="d-flex flex-column h-100">
|
<form
|
||||||
|
class="d-flex flex-column h-100"
|
||||||
|
[formGroup]="tagForm"
|
||||||
|
(keyup.enter)="tagForm.valid && onSubmit()"
|
||||||
|
(ngSubmit)="onSubmit()"
|
||||||
|
>
|
||||||
<h1 *ngIf="data.tag.id" i18n mat-dialog-title>Update tag</h1>
|
<h1 *ngIf="data.tag.id" i18n mat-dialog-title>Update tag</h1>
|
||||||
<h1 *ngIf="!data.tag.id" i18n mat-dialog-title>Add tag</h1>
|
<h1 *ngIf="!data.tag.id" i18n mat-dialog-title>Add tag</h1>
|
||||||
<div class="flex-grow-1 py-3" mat-dialog-content>
|
<div class="flex-grow-1 py-3" mat-dialog-content>
|
||||||
<div>
|
<div>
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
<mat-label i18n>Name</mat-label>
|
<mat-label i18n>Name</mat-label>
|
||||||
<input matInput name="name" required [(ngModel)]="data.tag.name" />
|
<input
|
||||||
|
formControlName="name"
|
||||||
|
matInput
|
||||||
|
(keydown.enter)="$event.stopPropagation()"
|
||||||
|
/>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="justify-content-end" mat-dialog-actions>
|
<div class="justify-content-end" mat-dialog-actions>
|
||||||
<button i18n mat-button (click)="onCancel()">Cancel</button>
|
<button i18n mat-button type="button" (click)="onCancel()">Cancel</button>
|
||||||
<button
|
<button
|
||||||
color="primary"
|
color="primary"
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[disabled]="!addTagForm.form.valid"
|
type="submit"
|
||||||
[mat-dialog-close]="data"
|
[disabled]="!tagForm.valid"
|
||||||
>
|
>
|
||||||
<ng-container i18n>Save</ng-container>
|
<ng-container i18n>Save</ng-container>
|
||||||
</button>
|
</button>
|
||||||
|
@ -116,7 +116,12 @@
|
|||||||
#assistantTrigger="matMenuTrigger"
|
#assistantTrigger="matMenuTrigger"
|
||||||
class="h-100 no-min-width px-2"
|
class="h-100 no-min-width px-2"
|
||||||
mat-button
|
mat-button
|
||||||
|
matBadge="✓"
|
||||||
|
matBadgeSize="small"
|
||||||
[mat-menu-trigger-for]="assistantMenu"
|
[mat-menu-trigger-for]="assistantMenu"
|
||||||
|
[matBadgeHidden]="
|
||||||
|
!hasFilters || !user?.settings?.isExperimentalFeatures
|
||||||
|
"
|
||||||
[matMenuTriggerRestoreFocus]="false"
|
[matMenuTriggerRestoreFocus]="false"
|
||||||
(menuOpened)="onOpenAssistant()"
|
(menuOpened)="onOpenAssistant()"
|
||||||
>
|
>
|
||||||
|
@ -28,6 +28,17 @@
|
|||||||
text-underline-offset: 0.25rem;
|
text-underline-offset: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.mat-badge {
|
||||||
|
::ng-deep {
|
||||||
|
.mat-badge-content {
|
||||||
|
--mat-badge-small-size-container-overlap-offset: -0.9rem;
|
||||||
|
--mat-badge-small-size-text-size: 0;
|
||||||
|
|
||||||
|
transform: scale(0.45);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ion-icon {
|
ion-icon {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
|
|
||||||
|
@ -65,6 +65,7 @@ export class HeaderComponent implements OnChanges {
|
|||||||
@ViewChild('assistant') assistantElement: GfAssistantComponent;
|
@ViewChild('assistant') assistantElement: GfAssistantComponent;
|
||||||
@ViewChild('assistantTrigger') assistentMenuTriggerElement: MatMenuTrigger;
|
@ViewChild('assistantTrigger') assistentMenuTriggerElement: MatMenuTrigger;
|
||||||
|
|
||||||
|
public hasFilters: boolean;
|
||||||
public hasPermissionForSocialLogin: boolean;
|
public hasPermissionForSocialLogin: boolean;
|
||||||
public hasPermissionForSubscription: boolean;
|
public hasPermissionForSubscription: boolean;
|
||||||
public hasPermissionToAccessAdminControl: boolean;
|
public hasPermissionToAccessAdminControl: boolean;
|
||||||
@ -106,6 +107,8 @@ export class HeaderComponent implements OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ngOnChanges() {
|
public ngOnChanges() {
|
||||||
|
this.hasFilters = this.userService.hasFilters();
|
||||||
|
|
||||||
this.hasPermissionForSocialLogin = hasPermission(
|
this.hasPermissionForSocialLogin = hasPermission(
|
||||||
this.info?.globalPermissions,
|
this.info?.globalPermissions,
|
||||||
permissions.enableSocialLogin
|
permissions.enableSocialLogin
|
||||||
|
@ -5,6 +5,7 @@ import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
|
|||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
|
import { MatBadgeModule } from '@angular/material/badge';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatMenuModule } from '@angular/material/menu';
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||||
@ -21,6 +22,7 @@ import { HeaderComponent } from './header.component';
|
|||||||
GfLogoComponent,
|
GfLogoComponent,
|
||||||
GfPremiumIndicatorComponent,
|
GfPremiumIndicatorComponent,
|
||||||
LoginWithAccessTokenDialogModule,
|
LoginWithAccessTokenDialogModule,
|
||||||
|
MatBadgeModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatMenuModule,
|
MatMenuModule,
|
||||||
MatToolbarModule,
|
MatToolbarModule,
|
||||||
|
@ -1,33 +1,30 @@
|
|||||||
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
|
|
||||||
import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.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';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
import { Position, User } from '@ghostfolio/common/interfaces';
|
import { PortfolioPosition, 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 { HoldingType, ToggleOption } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
|
||||||
import { DataSource } from '@prisma/client';
|
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
import { PositionDetailDialogParams } from '../position/position-detail-dialog/interfaces/interfaces';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'gf-home-holdings',
|
selector: 'gf-home-holdings',
|
||||||
styleUrls: ['./home-holdings.scss'],
|
styleUrls: ['./home-holdings.scss'],
|
||||||
templateUrl: './home-holdings.html'
|
templateUrl: './home-holdings.html'
|
||||||
})
|
})
|
||||||
export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
||||||
public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS;
|
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
public hasImpersonationId: boolean;
|
public hasImpersonationId: boolean;
|
||||||
public hasPermissionToCreateOrder: boolean;
|
public hasPermissionToCreateOrder: boolean;
|
||||||
public positions: Position[];
|
public holdings: PortfolioPosition[];
|
||||||
|
public holdingType: HoldingType = 'ACTIVE';
|
||||||
|
public holdingTypeOptions: ToggleOption[] = [
|
||||||
|
{ label: $localize`Active`, value: 'ACTIVE' },
|
||||||
|
{ label: $localize`Closed`, value: 'CLOSED' }
|
||||||
|
];
|
||||||
public user: User;
|
public user: User;
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
@ -36,25 +33,18 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
|||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
private deviceService: DeviceDetectorService,
|
private deviceService: DeviceDetectorService,
|
||||||
private dialog: MatDialog,
|
|
||||||
private impersonationStorageService: ImpersonationStorageService,
|
private impersonationStorageService: ImpersonationStorageService,
|
||||||
private route: ActivatedRoute,
|
|
||||||
private router: Router,
|
|
||||||
private userService: UserService
|
private userService: UserService
|
||||||
) {
|
) {}
|
||||||
this.route.queryParams
|
|
||||||
|
public ngOnInit() {
|
||||||
|
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||||
|
|
||||||
|
this.impersonationStorageService
|
||||||
|
.onChangeHasImpersonation()
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((params) => {
|
.subscribe((impersonationId) => {
|
||||||
if (
|
this.hasImpersonationId = !!impersonationId;
|
||||||
params['dataSource'] &&
|
|
||||||
params['positionDetailDialog'] &&
|
|
||||||
params['symbol']
|
|
||||||
) {
|
|
||||||
this.openPositionDialog({
|
|
||||||
dataSource: params['dataSource'],
|
|
||||||
symbol: params['symbol']
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.userService.stateChanged
|
this.userService.stateChanged
|
||||||
@ -68,37 +58,32 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
|||||||
permissions.createOrder
|
permissions.createOrder
|
||||||
);
|
);
|
||||||
|
|
||||||
this.update();
|
this.holdings = undefined;
|
||||||
|
|
||||||
|
this.fetchHoldings()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(({ holdings }) => {
|
||||||
|
this.holdings = holdings;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnInit() {
|
public onChangeHoldingType(aHoldingType: HoldingType) {
|
||||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
this.holdingType = aHoldingType;
|
||||||
|
|
||||||
this.impersonationStorageService
|
this.holdings = undefined;
|
||||||
.onChangeHasImpersonation()
|
|
||||||
|
this.fetchHoldings()
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((impersonationId) => {
|
.subscribe(({ holdings }) => {
|
||||||
this.hasImpersonationId = !!impersonationId;
|
this.holdings = holdings;
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public onChangeDateRange(dateRange: DateRange) {
|
this.changeDetectorRef.markForCheck();
|
||||||
this.dataService
|
|
||||||
.putUserSetting({ dateRange })
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe(() => {
|
|
||||||
this.userService.remove();
|
|
||||||
|
|
||||||
this.userService
|
|
||||||
.get()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe((user) => {
|
|
||||||
this.user = user;
|
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,59 +92,16 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
|||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
private openPositionDialog({
|
private fetchHoldings() {
|
||||||
dataSource,
|
const filters = this.userService.getFilters();
|
||||||
symbol
|
|
||||||
}: {
|
|
||||||
dataSource: DataSource;
|
|
||||||
symbol: string;
|
|
||||||
}) {
|
|
||||||
this.userService
|
|
||||||
.get()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe((user) => {
|
|
||||||
this.user = user;
|
|
||||||
|
|
||||||
const dialogRef = this.dialog.open(PositionDetailDialog, {
|
if (this.holdingType === 'CLOSED') {
|
||||||
autoFocus: false,
|
filters.push({ id: 'CLOSED', type: 'HOLDING_TYPE' });
|
||||||
data: <PositionDetailDialogParams>{
|
}
|
||||||
dataSource,
|
|
||||||
symbol,
|
|
||||||
baseCurrency: this.user?.settings?.baseCurrency,
|
|
||||||
colorScheme: this.user?.settings?.colorScheme,
|
|
||||||
deviceType: this.deviceType,
|
|
||||||
hasImpersonationId: this.hasImpersonationId,
|
|
||||||
hasPermissionToReportDataGlitch: hasPermission(
|
|
||||||
this.user?.permissions,
|
|
||||||
permissions.reportDataGlitch
|
|
||||||
),
|
|
||||||
locale: this.user?.settings?.locale
|
|
||||||
},
|
|
||||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
|
||||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
|
||||||
});
|
|
||||||
|
|
||||||
dialogRef
|
return this.dataService.fetchPortfolioHoldings({
|
||||||
.afterClosed()
|
filters,
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
range: this.user?.settings?.dateRange
|
||||||
.subscribe(() => {
|
});
|
||||||
this.router.navigate(['.'], { relativeTo: this.route });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private update() {
|
|
||||||
this.positions = undefined;
|
|
||||||
|
|
||||||
this.dataService
|
|
||||||
.fetchPositions({ range: this.user?.settings?.dateRange })
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe(({ positions }) => {
|
|
||||||
this.positions = positions;
|
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,27 +1,38 @@
|
|||||||
<div class="container justify-content-center p-3">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="align-items-center col-xs-12 col-md-8 offset-md-2">
|
<div class="col">
|
||||||
<mat-card appearance="outlined">
|
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Holdings</h1>
|
||||||
<mat-card-content class="p-0">
|
</div>
|
||||||
<gf-positions
|
</div>
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
<div class="row">
|
||||||
[deviceType]="deviceType"
|
<div class="col-lg">
|
||||||
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
<div class="d-flex justify-content-end">
|
||||||
[locale]="user?.settings?.locale"
|
<gf-toggle
|
||||||
[positions]="positions"
|
class="d-none d-lg-block"
|
||||||
[range]="user?.settings?.dateRange"
|
[defaultValue]="holdingType"
|
||||||
/>
|
[isLoading]="false"
|
||||||
</mat-card-content>
|
[options]="holdingTypeOptions"
|
||||||
</mat-card>
|
(change)="onChangeHoldingType($event.value)"
|
||||||
<div *ngIf="hasPermissionToCreateOrder" class="text-center">
|
/>
|
||||||
<a
|
|
||||||
class="mt-3"
|
|
||||||
i18n
|
|
||||||
mat-stroked-button
|
|
||||||
[routerLink]="['/portfolio', 'activities']"
|
|
||||||
>Manage Activities</a
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
|
<gf-holdings-table
|
||||||
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
|
[deviceType]="deviceType"
|
||||||
|
[hasPermissionToCreateActivity]="hasPermissionToCreateOrder"
|
||||||
|
[holdings]="holdings"
|
||||||
|
[locale]="user?.settings?.locale"
|
||||||
|
/>
|
||||||
|
@if (hasPermissionToCreateOrder && holdings?.length > 0) {
|
||||||
|
<div class="text-center">
|
||||||
|
<a
|
||||||
|
class="mt-3"
|
||||||
|
i18n
|
||||||
|
mat-stroked-button
|
||||||
|
[routerLink]="['/portfolio', 'activities']"
|
||||||
|
>Manage Activities</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
import { GfPositionDetailDialogModule } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.module';
|
|
||||||
import { GfPositionsModule } from '@ghostfolio/client/components/positions/positions.module';
|
|
||||||
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
||||||
|
import { GfHoldingsTableComponent } from '@ghostfolio/ui/holdings-table';
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
import { HomeHoldingsComponent } from './home-holdings.component';
|
import { HomeHoldingsComponent } from './home-holdings.component';
|
||||||
@ -14,11 +12,9 @@ import { HomeHoldingsComponent } from './home-holdings.component';
|
|||||||
declarations: [HomeHoldingsComponent],
|
declarations: [HomeHoldingsComponent],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
GfPositionDetailDialogModule,
|
GfHoldingsTableComponent,
|
||||||
GfPositionsModule,
|
|
||||||
GfToggleModule,
|
GfToggleModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatCardModule,
|
|
||||||
RouterModule
|
RouterModule
|
||||||
],
|
],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
[colorScheme]="user?.settings?.colorScheme"
|
[colorScheme]="user?.settings?.colorScheme"
|
||||||
[historicalDataItems]="historicalDataItems"
|
[historicalDataItems]="historicalDataItems"
|
||||||
[isAnimated]="true"
|
[isAnimated]="true"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale || undefined"
|
||||||
[showXAxis]="true"
|
[showXAxis]="true"
|
||||||
[showYAxis]="true"
|
[showYAxis]="true"
|
||||||
[yMax]="100"
|
[yMax]="100"
|
||||||
@ -30,7 +30,7 @@
|
|||||||
<div class="col-xs-12 col-md-8 offset-md-2">
|
<div class="col-xs-12 col-md-8 offset-md-2">
|
||||||
<gf-benchmark
|
<gf-benchmark
|
||||||
[benchmarks]="benchmarks"
|
[benchmarks]="benchmarks"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale || undefined"
|
||||||
[user]="user"
|
[user]="user"
|
||||||
/>
|
/>
|
||||||
<ngx-skeleton-loader
|
<ngx-skeleton-loader
|
||||||
|
@ -41,9 +41,7 @@
|
|||||||
[isCurrency]="true"
|
[isCurrency]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[value]="
|
[value]="
|
||||||
isLoading
|
isLoading ? undefined : performance?.netPerformanceWithCurrencyEffect
|
||||||
? undefined
|
|
||||||
: performance?.currentNetPerformanceWithCurrencyEffect
|
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -55,7 +53,7 @@
|
|||||||
[value]="
|
[value]="
|
||||||
isLoading
|
isLoading
|
||||||
? undefined
|
? undefined
|
||||||
: performance?.currentNetPerformancePercentWithCurrencyEffect
|
: performance?.netPerformancePercentageWithCurrencyEffect
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -49,12 +49,12 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
|
|||||||
this.value.nativeElement.innerHTML = '';
|
this.value.nativeElement.innerHTML = '';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (isNumber(this.performance?.currentValue)) {
|
if (isNumber(this.performance?.currentValueInBaseCurrency)) {
|
||||||
new CountUp('value', this.performance?.currentValue, {
|
new CountUp('value', this.performance?.currentValueInBaseCurrency, {
|
||||||
decimal: getNumberFormatDecimal(this.locale),
|
decimal: getNumberFormatDecimal(this.locale),
|
||||||
decimalPlaces:
|
decimalPlaces:
|
||||||
this.deviceType === 'mobile' &&
|
this.deviceType === 'mobile' &&
|
||||||
this.performance?.currentValue >= 100000
|
this.performance?.currentValueInBaseCurrency >= 100000
|
||||||
? 0
|
? 0
|
||||||
: 2,
|
: 2,
|
||||||
duration: 1,
|
duration: 1,
|
||||||
@ -63,8 +63,7 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
|
|||||||
} else if (this.showDetails === false) {
|
} else if (this.showDetails === false) {
|
||||||
new CountUp(
|
new CountUp(
|
||||||
'value',
|
'value',
|
||||||
this.performance?.currentNetPerformancePercentWithCurrencyEffect *
|
this.performance?.netPerformancePercentageWithCurrencyEffect * 100,
|
||||||
100,
|
|
||||||
{
|
{
|
||||||
decimal: getNumberFormatDecimal(this.locale),
|
decimal: getNumberFormatDecimal(this.locale),
|
||||||
decimalPlaces: 2,
|
decimalPlaces: 2,
|
||||||
|
@ -9,9 +9,19 @@
|
|||||||
class="flex-nowrap px-3 py-1 row"
|
class="flex-nowrap px-3 py-1 row"
|
||||||
[hidden]="summary?.ordersCount === null"
|
[hidden]="summary?.ordersCount === null"
|
||||||
>
|
>
|
||||||
<div class="flex-grow-1 ml-3 text-truncate" i18n>
|
<div class="d-flex flex-grow-1 ml-3 text-truncate">
|
||||||
{{ summary?.ordersCount }}
|
{{ summary?.ordersCount }}
|
||||||
{summary?.ordersCount, plural, =1 {transaction} other {transactions}}
|
<ng-container i18n>{summary?.ordersCount, plural,
|
||||||
|
=1 {activity}
|
||||||
|
other {activities}
|
||||||
|
}</ng-container>
|
||||||
|
<span
|
||||||
|
class="align-items-center d-flex ml-1"
|
||||||
|
matTooltipPosition="above"
|
||||||
|
[matTooltip]="buyAndSellActivitiesTooltip"
|
||||||
|
>
|
||||||
|
<ion-icon name="information-circle-outline" />
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -65,9 +75,7 @@
|
|||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[unit]="baseCurrency"
|
[unit]="baseCurrency"
|
||||||
[value]="
|
[value]="
|
||||||
isLoading
|
isLoading ? undefined : summary?.grossPerformanceWithCurrencyEffect
|
||||||
? undefined
|
|
||||||
: summary?.currentGrossPerformanceWithCurrencyEffect
|
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -91,7 +99,7 @@
|
|||||||
[value]="
|
[value]="
|
||||||
isLoading
|
isLoading
|
||||||
? undefined
|
? undefined
|
||||||
: summary?.currentGrossPerformancePercentWithCurrencyEffect
|
: summary?.grossPerformancePercentageWithCurrencyEffect
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -121,9 +129,7 @@
|
|||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[unit]="baseCurrency"
|
[unit]="baseCurrency"
|
||||||
[value]="
|
[value]="
|
||||||
isLoading
|
isLoading ? undefined : summary?.netPerformanceWithCurrencyEffect
|
||||||
? undefined
|
|
||||||
: summary?.currentNetPerformanceWithCurrencyEffect
|
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -147,7 +153,7 @@
|
|||||||
[value]="
|
[value]="
|
||||||
isLoading
|
isLoading
|
||||||
? undefined
|
? undefined
|
||||||
: summary?.currentNetPerformancePercentWithCurrencyEffect
|
: summary?.netPerformancePercentageWithCurrencyEffect
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -164,7 +170,7 @@
|
|||||||
[isCurrency]="true"
|
[isCurrency]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[unit]="baseCurrency"
|
[unit]="baseCurrency"
|
||||||
[value]="isLoading ? undefined : summary?.currentValue"
|
[value]="isLoading ? undefined : summary?.currentValueInBaseCurrency"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { getDateFnsLocale, getLocale } from '@ghostfolio/common/helper';
|
import { getDateFnsLocale, getLocale } from '@ghostfolio/common/helper';
|
||||||
import { PortfolioSummary } from '@ghostfolio/common/interfaces';
|
import { PortfolioSummary } from '@ghostfolio/common/interfaces';
|
||||||
|
import { translate } from '@ghostfolio/ui/i18n';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
@ -28,6 +29,9 @@ export class PortfolioSummaryComponent implements OnChanges, OnInit {
|
|||||||
|
|
||||||
@Output() emergencyFundChanged = new EventEmitter<number>();
|
@Output() emergencyFundChanged = new EventEmitter<number>();
|
||||||
|
|
||||||
|
public buyAndSellActivitiesTooltip = translate(
|
||||||
|
'BUY_AND_SELL_ACTIVITIES_TOOLTIP'
|
||||||
|
);
|
||||||
public timeInMarket: string;
|
public timeInMarket: string;
|
||||||
|
|
||||||
public constructor() {}
|
public constructor() {}
|
||||||
|
@ -2,13 +2,14 @@ import { GfValueComponent } from '@ghostfolio/ui/value';
|
|||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||||
|
|
||||||
import { PortfolioSummaryComponent } from './portfolio-summary.component';
|
import { PortfolioSummaryComponent } from './portfolio-summary.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [PortfolioSummaryComponent],
|
declarations: [PortfolioSummaryComponent],
|
||||||
exports: [PortfolioSummaryComponent],
|
exports: [PortfolioSummaryComponent],
|
||||||
imports: [CommonModule, GfValueComponent],
|
imports: [CommonModule, GfValueComponent, MatTooltipModule],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
export class GfPortfolioSummaryModule {}
|
export class GfPortfolioSummaryModule {}
|
||||||
|
@ -56,6 +56,8 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
|||||||
public marketPrice: number;
|
public marketPrice: number;
|
||||||
public maxPrice: number;
|
public maxPrice: number;
|
||||||
public minPrice: number;
|
public minPrice: number;
|
||||||
|
public netPerformance: number;
|
||||||
|
public netPerformancePercent: number;
|
||||||
public netPerformancePercentWithCurrencyEffect: number;
|
public netPerformancePercentWithCurrencyEffect: number;
|
||||||
public netPerformanceWithCurrencyEffect: number;
|
public netPerformanceWithCurrencyEffect: number;
|
||||||
public quantity: number;
|
public quantity: number;
|
||||||
@ -104,6 +106,8 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
|||||||
marketPrice,
|
marketPrice,
|
||||||
maxPrice,
|
maxPrice,
|
||||||
minPrice,
|
minPrice,
|
||||||
|
netPerformance,
|
||||||
|
netPerformancePercent,
|
||||||
netPerformancePercentWithCurrencyEffect,
|
netPerformancePercentWithCurrencyEffect,
|
||||||
netPerformanceWithCurrencyEffect,
|
netPerformanceWithCurrencyEffect,
|
||||||
orders,
|
orders,
|
||||||
@ -126,15 +130,15 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
|||||||
this.feeInBaseCurrency = feeInBaseCurrency;
|
this.feeInBaseCurrency = feeInBaseCurrency;
|
||||||
this.firstBuyDate = firstBuyDate;
|
this.firstBuyDate = firstBuyDate;
|
||||||
this.historicalDataItems = historicalData.map(
|
this.historicalDataItems = historicalData.map(
|
||||||
(historicalDataItem) => {
|
({ averagePrice, date, marketPrice }) => {
|
||||||
this.benchmarkDataItems.push({
|
this.benchmarkDataItems.push({
|
||||||
date: historicalDataItem.date,
|
date,
|
||||||
value: historicalDataItem.averagePrice
|
value: averagePrice
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
date: historicalDataItem.date,
|
date,
|
||||||
value: historicalDataItem.marketPrice
|
value: marketPrice
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -142,6 +146,8 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
|||||||
this.marketPrice = marketPrice;
|
this.marketPrice = marketPrice;
|
||||||
this.maxPrice = maxPrice;
|
this.maxPrice = maxPrice;
|
||||||
this.minPrice = minPrice;
|
this.minPrice = minPrice;
|
||||||
|
this.netPerformance = netPerformance;
|
||||||
|
this.netPerformancePercent = netPerformancePercent;
|
||||||
this.netPerformancePercentWithCurrencyEffect =
|
this.netPerformancePercentWithCurrencyEffect =
|
||||||
netPerformancePercentWithCurrencyEffect;
|
netPerformancePercentWithCurrencyEffect;
|
||||||
this.netPerformanceWithCurrencyEffect =
|
this.netPerformanceWithCurrencyEffect =
|
@ -37,27 +37,58 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
<gf-value
|
@if (
|
||||||
i18n
|
SymbolProfile?.currency &&
|
||||||
size="medium"
|
data.baseCurrency !== SymbolProfile?.currency
|
||||||
[colorizeSign]="true"
|
) {
|
||||||
[isCurrency]="true"
|
<gf-value
|
||||||
[locale]="data.locale"
|
i18n
|
||||||
[unit]="data.baseCurrency"
|
size="medium"
|
||||||
[value]="netPerformanceWithCurrencyEffect"
|
[colorizeSign]="true"
|
||||||
>Change</gf-value
|
[isCurrency]="true"
|
||||||
>
|
[locale]="data.locale"
|
||||||
|
[unit]="data.baseCurrency"
|
||||||
|
[value]="netPerformanceWithCurrencyEffect"
|
||||||
|
>Change with currency effect</gf-value
|
||||||
|
>
|
||||||
|
} @else {
|
||||||
|
<gf-value
|
||||||
|
i18n
|
||||||
|
size="medium"
|
||||||
|
[colorizeSign]="true"
|
||||||
|
[isCurrency]="true"
|
||||||
|
[locale]="data.locale"
|
||||||
|
[unit]="data.baseCurrency"
|
||||||
|
[value]="netPerformance"
|
||||||
|
>Change</gf-value
|
||||||
|
>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
<gf-value
|
@if (
|
||||||
i18n
|
SymbolProfile?.currency &&
|
||||||
size="medium"
|
data.baseCurrency !== SymbolProfile?.currency
|
||||||
[colorizeSign]="true"
|
) {
|
||||||
[isPercent]="true"
|
<gf-value
|
||||||
[locale]="data.locale"
|
i18n
|
||||||
[value]="netPerformancePercentWithCurrencyEffect"
|
size="medium"
|
||||||
>Performance</gf-value
|
[colorizeSign]="true"
|
||||||
>
|
[isPercent]="true"
|
||||||
|
[locale]="data.locale"
|
||||||
|
[value]="netPerformancePercentWithCurrencyEffect"
|
||||||
|
>Performance with currency effect</gf-value
|
||||||
|
>
|
||||||
|
} @else {
|
||||||
|
<gf-value
|
||||||
|
i18n
|
||||||
|
size="medium"
|
||||||
|
[colorizeSign]="true"
|
||||||
|
[isPercent]="true"
|
||||||
|
[locale]="data.locale"
|
||||||
|
[value]="netPerformancePercent"
|
||||||
|
>Performance</gf-value
|
||||||
|
>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
<gf-value
|
<gf-value
|
||||||
@ -304,6 +335,7 @@
|
|||||||
[dataSource]="dataSource"
|
[dataSource]="dataSource"
|
||||||
[deviceType]="data.deviceType"
|
[deviceType]="data.deviceType"
|
||||||
[hasPermissionToCreateActivity]="false"
|
[hasPermissionToCreateActivity]="false"
|
||||||
|
[hasPermissionToDeleteActivity]="false"
|
||||||
[hasPermissionToExportActivities]="
|
[hasPermissionToExportActivities]="
|
||||||
!data.hasImpersonationId && !user?.settings?.isRestrictedView
|
!data.hasImpersonationId && !user?.settings?.isRestrictedView
|
||||||
"
|
"
|
@ -1,72 +0,0 @@
|
|||||||
<div class="container p-0">
|
|
||||||
<div class="flex-nowrap no-gutters row">
|
|
||||||
<a
|
|
||||||
class="d-flex p-3 w-100"
|
|
||||||
[ngClass]="{ 'cursor-default': isLoading }"
|
|
||||||
[queryParams]="{
|
|
||||||
dataSource: position?.dataSource,
|
|
||||||
positionDetailDialog: true,
|
|
||||||
symbol: position?.symbol
|
|
||||||
}"
|
|
||||||
[routerLink]="[]"
|
|
||||||
>
|
|
||||||
<div class="d-flex mr-2">
|
|
||||||
<gf-trend-indicator
|
|
||||||
class="d-flex"
|
|
||||||
size="large"
|
|
||||||
[isLoading]="isLoading"
|
|
||||||
[marketState]="position?.marketState"
|
|
||||||
[range]="range"
|
|
||||||
[value]="position?.netPerformancePercentageWithCurrencyEffect"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div *ngIf="isLoading" class="flex-grow-1">
|
|
||||||
<ngx-skeleton-loader
|
|
||||||
animation="pulse"
|
|
||||||
class="mb-1"
|
|
||||||
[theme]="{
|
|
||||||
height: '1.2rem',
|
|
||||||
width: '12rem'
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
<ngx-skeleton-loader
|
|
||||||
animation="pulse"
|
|
||||||
[theme]="{
|
|
||||||
height: '1rem',
|
|
||||||
width: '8rem'
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div *ngIf="!isLoading" class="flex-grow-1 text-truncate">
|
|
||||||
<div class="h6 m-0 text-truncate">{{ position?.name }}</div>
|
|
||||||
<div class="d-flex">
|
|
||||||
<small class="text-muted">{{ position?.symbol | gfSymbol }}</small>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex mt-1">
|
|
||||||
<gf-value
|
|
||||||
class="mr-3"
|
|
||||||
[colorizeSign]="true"
|
|
||||||
[isCurrency]="true"
|
|
||||||
[locale]="locale"
|
|
||||||
[unit]="baseCurrency"
|
|
||||||
[value]="position?.netPerformanceWithCurrencyEffect"
|
|
||||||
/>
|
|
||||||
<gf-value
|
|
||||||
[colorizeSign]="true"
|
|
||||||
[isPercent]="true"
|
|
||||||
[locale]="locale"
|
|
||||||
[value]="position?.netPerformancePercentageWithCurrencyEffect"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="align-items-center d-flex">
|
|
||||||
<ion-icon
|
|
||||||
*ngIf="!isLoading"
|
|
||||||
class="chevron text-muted"
|
|
||||||
name="chevron-forward-outline"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,13 +0,0 @@
|
|||||||
:host {
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
.container {
|
|
||||||
gf-trend-indicator {
|
|
||||||
padding-top: 0.15rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chevron {
|
|
||||||
opacity: 0.33;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
|
||||||
import { getLocale } from '@ghostfolio/common/helper';
|
|
||||||
import { Position } from '@ghostfolio/common/interfaces';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ChangeDetectionStrategy,
|
|
||||||
Component,
|
|
||||||
Input,
|
|
||||||
OnDestroy,
|
|
||||||
OnInit
|
|
||||||
} from '@angular/core';
|
|
||||||
import { Subject } from 'rxjs';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'gf-position',
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
templateUrl: './position.component.html',
|
|
||||||
styleUrls: ['./position.component.scss']
|
|
||||||
})
|
|
||||||
export class PositionComponent implements OnDestroy, OnInit {
|
|
||||||
@Input() baseCurrency: string;
|
|
||||||
@Input() deviceType: string;
|
|
||||||
@Input() isLoading: boolean;
|
|
||||||
@Input() locale = getLocale();
|
|
||||||
@Input() position: Position;
|
|
||||||
@Input() range: string;
|
|
||||||
|
|
||||||
public unknownKey = UNKNOWN_KEY;
|
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
|
||||||
|
|
||||||
public constructor() {}
|
|
||||||
|
|
||||||
public ngOnInit() {}
|
|
||||||
|
|
||||||
public ngOnDestroy() {
|
|
||||||
this.unsubscribeSubject.next();
|
|
||||||
this.unsubscribeSubject.complete();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
|
||||||
import { GfTrendIndicatorComponent } from '@ghostfolio/ui/trend-indicator';
|
|
||||||
import { GfValueComponent } from '@ghostfolio/ui/value';
|
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|
||||||
import { MatDialogModule } from '@angular/material/dialog';
|
|
||||||
import { RouterModule } from '@angular/router';
|
|
||||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
|
||||||
|
|
||||||
import { GfPositionDetailDialogModule } from './position-detail-dialog/position-detail-dialog.module';
|
|
||||||
import { PositionComponent } from './position.component';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [PositionComponent],
|
|
||||||
exports: [PositionComponent],
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
GfPositionDetailDialogModule,
|
|
||||||
GfSymbolModule,
|
|
||||||
GfTrendIndicatorComponent,
|
|
||||||
GfValueComponent,
|
|
||||||
MatDialogModule,
|
|
||||||
NgxSkeletonLoaderModule,
|
|
||||||
RouterModule
|
|
||||||
],
|
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
|
||||||
})
|
|
||||||
export class GfPositionModule {}
|
|
@ -1,35 +0,0 @@
|
|||||||
<div class="container p-0">
|
|
||||||
<div class="row no-gutters">
|
|
||||||
<div class="col">
|
|
||||||
<ng-container *ngIf="positions === undefined">
|
|
||||||
<gf-position [isLoading]="true" />
|
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngIf="positions !== undefined">
|
|
||||||
<ng-container *ngIf="hasPositions">
|
|
||||||
<gf-position
|
|
||||||
*ngFor="let position of positionsWithPriority"
|
|
||||||
[baseCurrency]="baseCurrency"
|
|
||||||
[deviceType]="deviceType"
|
|
||||||
[locale]="locale"
|
|
||||||
[position]="position"
|
|
||||||
[range]="range"
|
|
||||||
/>
|
|
||||||
<gf-position
|
|
||||||
*ngFor="let position of positionsRest"
|
|
||||||
[baseCurrency]="baseCurrency"
|
|
||||||
[deviceType]="deviceType"
|
|
||||||
[locale]="locale"
|
|
||||||
[position]="position"
|
|
||||||
[range]="range"
|
|
||||||
/>
|
|
||||||
</ng-container>
|
|
||||||
<div
|
|
||||||
*ngIf="hasPermissionToCreateOrder && !hasPositions"
|
|
||||||
class="p-3 text-center"
|
|
||||||
>
|
|
||||||
<gf-no-transactions-info-indicator [hasBorder]="false" />
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,17 +0,0 @@
|
|||||||
:host {
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
gf-position {
|
|
||||||
&:nth-child(even) {
|
|
||||||
background-color: rgba(0, 0, 0, var(--gf-theme-alpha-hover));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:host-context(.is-dark-theme) {
|
|
||||||
gf-position {
|
|
||||||
&:nth-child(even) {
|
|
||||||
background-color: rgba(255, 255, 255, var(--gf-theme-alpha-hover));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,70 +0,0 @@
|
|||||||
import { getLocale } from '@ghostfolio/common/helper';
|
|
||||||
import { Position } from '@ghostfolio/common/interfaces';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ChangeDetectionStrategy,
|
|
||||||
Component,
|
|
||||||
Input,
|
|
||||||
OnChanges,
|
|
||||||
OnInit
|
|
||||||
} from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'gf-positions',
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
templateUrl: './positions.component.html',
|
|
||||||
styleUrls: ['./positions.component.scss']
|
|
||||||
})
|
|
||||||
export class PositionsComponent implements OnChanges, OnInit {
|
|
||||||
@Input() baseCurrency: string;
|
|
||||||
@Input() deviceType: string;
|
|
||||||
@Input() hasPermissionToCreateOrder: boolean;
|
|
||||||
@Input() locale = getLocale();
|
|
||||||
@Input() positions: Position[];
|
|
||||||
@Input() range: string;
|
|
||||||
|
|
||||||
public hasPositions: boolean;
|
|
||||||
public positionsRest: Position[] = [];
|
|
||||||
public positionsWithPriority: Position[] = [];
|
|
||||||
|
|
||||||
public constructor() {}
|
|
||||||
|
|
||||||
public ngOnInit() {}
|
|
||||||
|
|
||||||
public ngOnChanges() {
|
|
||||||
if (this.positions) {
|
|
||||||
this.hasPositions = this.positions.length > 0;
|
|
||||||
|
|
||||||
if (!this.hasPositions) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.positionsRest = [];
|
|
||||||
this.positionsWithPriority = [];
|
|
||||||
|
|
||||||
for (const portfolioPosition of this.positions) {
|
|
||||||
if (portfolioPosition.marketState === 'open' || this.range !== '1d') {
|
|
||||||
// Only show positions where the market is open in today's view
|
|
||||||
this.positionsWithPriority.push(portfolioPosition);
|
|
||||||
} else {
|
|
||||||
this.positionsRest.push(portfolioPosition);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.positionsRest.sort((a, b) =>
|
|
||||||
(a.name || a.symbol)?.toLowerCase() >
|
|
||||||
(b.name || b.symbol)?.toLowerCase()
|
|
||||||
? 1
|
|
||||||
: -1
|
|
||||||
);
|
|
||||||
this.positionsWithPriority.sort((a, b) =>
|
|
||||||
(a.name || a.symbol)?.toLowerCase() >
|
|
||||||
(b.name || b.symbol)?.toLowerCase()
|
|
||||||
? 1
|
|
||||||
: -1
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.hasPositions = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
import { GfNoTransactionsInfoComponent } from '@ghostfolio/ui/no-transactions-info';
|
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
|
||||||
|
|
||||||
import { GfPositionModule } from '../position/position.module';
|
|
||||||
import { PositionsComponent } from './positions.component';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [PositionsComponent],
|
|
||||||
exports: [PositionsComponent],
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
GfNoTransactionsInfoComponent,
|
|
||||||
GfPositionModule,
|
|
||||||
MatButtonModule
|
|
||||||
],
|
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
|
||||||
})
|
|
||||||
export class GfPositionsModule {}
|
|
@ -6,7 +6,6 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
|
||||||
import { GfPositionModule } from '../position/position.module';
|
|
||||||
import { RulesComponent } from './rules.component';
|
import { RulesComponent } from './rules.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@ -15,7 +14,6 @@ import { RulesComponent } from './rules.component';
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
GfNoTransactionsInfoComponent,
|
GfNoTransactionsInfoComponent,
|
||||||
GfPositionModule,
|
|
||||||
GfRuleModule,
|
GfRuleModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatCardModule
|
MatCardModule
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto';
|
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 { validateObjectForForm } from '@ghostfolio/client/util/form.util';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
@ -40,22 +41,22 @@ export class CreateOrUpdateAccessDialog implements OnDestroy {
|
|||||||
alias: [this.data.access.alias],
|
alias: [this.data.access.alias],
|
||||||
permissions: [this.data.access.permissions[0], Validators.required],
|
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]
|
granteeUserId: [this.data.access.grantee, Validators.required]
|
||||||
});
|
});
|
||||||
|
|
||||||
this.accessForm.get('type').valueChanges.subscribe((accessType) => {
|
this.accessForm.get('type').valueChanges.subscribe((accessType) => {
|
||||||
|
const granteeUserIdControl = this.accessForm.get('granteeUserId');
|
||||||
const permissionsControl = this.accessForm.get('permissions');
|
const permissionsControl = this.accessForm.get('permissions');
|
||||||
const userIdControl = this.accessForm.get('userId');
|
|
||||||
|
|
||||||
if (accessType === 'PRIVATE') {
|
if (accessType === 'PRIVATE') {
|
||||||
|
granteeUserIdControl.setValidators(Validators.required);
|
||||||
permissionsControl.setValidators(Validators.required);
|
permissionsControl.setValidators(Validators.required);
|
||||||
userIdControl.setValidators(Validators.required);
|
|
||||||
} else {
|
} else {
|
||||||
userIdControl.clearValidators();
|
granteeUserIdControl.clearValidators();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
granteeUserIdControl.updateValueAndValidity();
|
||||||
permissionsControl.updateValueAndValidity();
|
permissionsControl.updateValueAndValidity();
|
||||||
userIdControl.updateValueAndValidity();
|
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
@ -65,28 +66,38 @@ export class CreateOrUpdateAccessDialog implements OnDestroy {
|
|||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
public onSubmit() {
|
public async onSubmit() {
|
||||||
const access: CreateAccessDto = {
|
const access: CreateAccessDto = {
|
||||||
alias: this.accessForm.get('alias').value,
|
alias: this.accessForm.get('alias').value,
|
||||||
granteeUserId: this.accessForm.get('userId').value,
|
granteeUserId: this.accessForm.get('granteeUserId').value,
|
||||||
permissions: [this.accessForm.get('permissions').value]
|
permissions: [this.accessForm.get('permissions').value]
|
||||||
};
|
};
|
||||||
|
|
||||||
this.dataService
|
try {
|
||||||
.postAccess(access)
|
await validateObjectForForm({
|
||||||
.pipe(
|
classDto: CreateAccessDto,
|
||||||
catchError((error) => {
|
form: this.accessForm,
|
||||||
if (error.status === StatusCodes.BAD_REQUEST) {
|
object: access
|
||||||
alert($localize`Oops! Could not grant access.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return EMPTY;
|
|
||||||
}),
|
|
||||||
takeUntil(this.unsubscribeSubject)
|
|
||||||
)
|
|
||||||
.subscribe(() => {
|
|
||||||
this.dialogRef.close({ access });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.dataService
|
||||||
|
.postAccess(access)
|
||||||
|
.pipe(
|
||||||
|
catchError((error) => {
|
||||||
|
if (error.status === StatusCodes.BAD_REQUEST) {
|
||||||
|
alert($localize`Oops! Could not grant access.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return EMPTY;
|
||||||
|
}),
|
||||||
|
takeUntil(this.unsubscribeSubject)
|
||||||
|
)
|
||||||
|
.subscribe(() => {
|
||||||
|
this.dialogRef.close(access);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
|
@ -45,7 +45,7 @@
|
|||||||
Ghostfolio <ng-container i18n>User ID</ng-container>
|
Ghostfolio <ng-container i18n>User ID</ng-container>
|
||||||
</mat-label>
|
</mat-label>
|
||||||
<input
|
<input
|
||||||
formControlName="userId"
|
formControlName="granteeUserId"
|
||||||
matInput
|
matInput
|
||||||
type="text"
|
type="text"
|
||||||
(keydown.enter)="$event.stopPropagation()"
|
(keydown.enter)="$event.stopPropagation()"
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
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';
|
||||||
@ -20,6 +21,7 @@ import { CreateOrUpdateAccessDialog } from './create-or-update-access-dialog/cre
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
host: { class: 'has-fab' },
|
||||||
selector: 'gf-user-account-access',
|
selector: 'gf-user-account-access',
|
||||||
styleUrls: ['./user-account-access.scss'],
|
styleUrls: ['./user-account-access.scss'],
|
||||||
templateUrl: './user-account-access.html'
|
templateUrl: './user-account-access.html'
|
||||||
@ -113,7 +115,7 @@ export class UserAccountAccessComponent implements OnDestroy, OnInit {
|
|||||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||||
});
|
});
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe((access) => {
|
dialogRef.afterClosed().subscribe((access: CreateAccessDto | null) => {
|
||||||
if (access) {
|
if (access) {
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
@ -54,9 +54,10 @@ export class AuthGuard {
|
|||||||
this.router.navigate(['/' + $localize`register`]);
|
this.router.navigate(['/' + $localize`register`]);
|
||||||
resolve(false);
|
resolve(false);
|
||||||
} else if (
|
} else if (
|
||||||
AuthGuard.PUBLIC_PAGE_ROUTES.filter((publicPageRoute) =>
|
AuthGuard.PUBLIC_PAGE_ROUTES.filter((publicPageRoute) => {
|
||||||
state.url.startsWith(publicPageRoute)
|
const [, url] = state.url.split('/');
|
||||||
)?.length > 0
|
return `/${url}` === publicPageRoute;
|
||||||
|
})?.length > 0
|
||||||
) {
|
) {
|
||||||
resolve(true);
|
resolve(true);
|
||||||
return EMPTY;
|
return EMPTY;
|
||||||
|
@ -77,7 +77,7 @@
|
|||||||
mat-icon-button
|
mat-icon-button
|
||||||
title="Follow Ghostfolio on X (formerly Twitter)"
|
title="Follow Ghostfolio on X (formerly Twitter)"
|
||||||
>
|
>
|
||||||
<span class="line-height-1 text-center w-100">𝕏</span>
|
<ion-icon name="logo-x" />
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
*ngIf="user?.subscription?.type === 'Premium'"
|
*ngIf="user?.subscription?.type === 'Premium'"
|
||||||
|
@ -21,7 +21,7 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog/c
|
|||||||
import { TransferBalanceDialog } from './transfer-balance/transfer-balance-dialog.component';
|
import { TransferBalanceDialog } from './transfer-balance/transfer-balance-dialog.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'page' },
|
host: { class: 'has-fab page' },
|
||||||
selector: 'gf-accounts-page',
|
selector: 'gf-accounts-page',
|
||||||
styleUrls: ['./accounts-page.scss'],
|
styleUrls: ['./accounts-page.scss'],
|
||||||
templateUrl: './accounts-page.html'
|
templateUrl: './accounts-page.html'
|
||||||
@ -189,9 +189,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
|||||||
dialogRef
|
dialogRef
|
||||||
.afterClosed()
|
.afterClosed()
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((data: any) => {
|
.subscribe((account: UpdateAccountDto | null) => {
|
||||||
const account: UpdateAccountDto = data?.account;
|
|
||||||
|
|
||||||
if (account) {
|
if (account) {
|
||||||
this.dataService
|
this.dataService
|
||||||
.putAccount(account)
|
.putAccount(account)
|
||||||
@ -258,9 +256,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
|||||||
dialogRef
|
dialogRef
|
||||||
.afterClosed()
|
.afterClosed()
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((data: any) => {
|
.subscribe((account: CreateAccountDto | null) => {
|
||||||
const account: CreateAccountDto = data?.account;
|
|
||||||
|
|
||||||
if (account) {
|
if (account) {
|
||||||
this.dataService
|
this.dataService
|
||||||
.postAccount(account)
|
.postAccount(account)
|
||||||
|
@ -123,6 +123,8 @@ export class CreateOrUpdateAccountDialog implements OnDestroy {
|
|||||||
form: this.accountForm,
|
form: this.accountForm,
|
||||||
object: account
|
object: account
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.dialogRef.close(account as UpdateAccountDto);
|
||||||
} else {
|
} else {
|
||||||
delete (account as CreateAccountDto).id;
|
delete (account as CreateAccountDto).id;
|
||||||
|
|
||||||
@ -131,9 +133,9 @@ export class CreateOrUpdateAccountDialog implements OnDestroy {
|
|||||||
form: this.accountForm,
|
form: this.accountForm,
|
||||||
object: account
|
object: account
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
this.dialogRef.close({ account });
|
this.dialogRef.close(account as CreateAccountDto);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
@ -30,8 +30,10 @@
|
|||||||
systems, including
|
systems, including
|
||||||
<a href="https://github.com/bigbeartechworld/big-bear-casaos"
|
<a href="https://github.com/bigbeartechworld/big-bear-casaos"
|
||||||
>CasaOS</a
|
>CasaOS</a
|
||||||
>, Home Assistant,
|
>,
|
||||||
<a href="https://www.runtipi.io/docs/apps-available">Runtipi</a>,
|
<a href="https://github.com/lildude/ha-addon-ghostfolio"
|
||||||
|
>Home Assistant</a
|
||||||
|
>, <a href="https://www.runtipi.io/docs/apps-available">Runtipi</a>,
|
||||||
<a href="https://truecharts.org/charts/stable/ghostfolio"
|
<a href="https://truecharts.org/charts/stable/ghostfolio"
|
||||||
>TrueCharts</a
|
>TrueCharts</a
|
||||||
>, <a href="https://apps.umbrel.com/app/ghostfolio">Umbrel</a>, and
|
>, <a href="https://apps.umbrel.com/app/ghostfolio">Umbrel</a>, and
|
||||||
|
@ -22,6 +22,11 @@ const routes: Routes = [
|
|||||||
component: HomeHoldingsComponent,
|
component: HomeHoldingsComponent,
|
||||||
title: $localize`Holdings`
|
title: $localize`Holdings`
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'holdings',
|
||||||
|
component: HomeHoldingsComponent,
|
||||||
|
title: $localize`Holdings`
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'summary',
|
path: 'summary',
|
||||||
component: HomeSummaryComponent,
|
component: HomeSummaryComponent,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
import { TabConfiguration, User } from '@ghostfolio/common/interfaces';
|
import { TabConfiguration, User } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
@ -14,6 +15,7 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
})
|
})
|
||||||
export class HomePageComponent implements OnDestroy, OnInit {
|
export class HomePageComponent implements OnDestroy, OnInit {
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
|
public hasImpersonationId: boolean;
|
||||||
public tabs: TabConfiguration[] = [];
|
public tabs: TabConfiguration[] = [];
|
||||||
public user: User;
|
public user: User;
|
||||||
|
|
||||||
@ -22,6 +24,7 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
|||||||
public constructor(
|
public constructor(
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private deviceService: DeviceDetectorService,
|
private deviceService: DeviceDetectorService,
|
||||||
|
private impersonationStorageService: ImpersonationStorageService,
|
||||||
private userService: UserService
|
private userService: UserService
|
||||||
) {
|
) {
|
||||||
this.userService.stateChanged
|
this.userService.stateChanged
|
||||||
@ -59,6 +62,13 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
public ngOnInit() {
|
public ngOnInit() {
|
||||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||||
|
|
||||||
|
this.impersonationStorageService
|
||||||
|
.onChangeHasImpersonation()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((impersonationId) => {
|
||||||
|
this.hasImpersonationId = !!impersonationId;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
||||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
||||||
import { PositionDetailDialogParams } from '@ghostfolio/client/components/position/position-detail-dialog/interfaces/interfaces';
|
|
||||||
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
|
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { IcsService } from '@ghostfolio/client/services/ics/ics.service';
|
import { IcsService } from '@ghostfolio/client/services/ics/ics.service';
|
||||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||||
@ -18,7 +16,7 @@ import { PageEvent } from '@angular/material/paginator';
|
|||||||
import { Sort, SortDirection } from '@angular/material/sort';
|
import { Sort, SortDirection } from '@angular/material/sort';
|
||||||
import { MatTableDataSource } from '@angular/material/table';
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { DataSource, Order as OrderModel } from '@prisma/client';
|
import { Order as OrderModel } from '@prisma/client';
|
||||||
import { format, parseISO } from 'date-fns';
|
import { format, parseISO } from 'date-fns';
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { Subject, Subscription } from 'rxjs';
|
import { Subject, Subscription } from 'rxjs';
|
||||||
@ -29,6 +27,7 @@ import { ImportActivitiesDialog } from './import-activities-dialog/import-activi
|
|||||||
import { ImportActivitiesDialogParams } from './import-activities-dialog/interfaces/interfaces';
|
import { ImportActivitiesDialogParams } from './import-activities-dialog/interfaces/interfaces';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
host: { class: 'has-fab' },
|
||||||
selector: 'gf-activities-page',
|
selector: 'gf-activities-page',
|
||||||
styleUrls: ['./activities-page.scss'],
|
styleUrls: ['./activities-page.scss'],
|
||||||
templateUrl: './activities-page.html'
|
templateUrl: './activities-page.html'
|
||||||
@ -82,15 +81,6 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
|||||||
} else {
|
} else {
|
||||||
this.router.navigate(['.'], { relativeTo: this.route });
|
this.router.navigate(['.'], { relativeTo: this.route });
|
||||||
}
|
}
|
||||||
} else if (
|
|
||||||
params['dataSource'] &&
|
|
||||||
params['positionDetailDialog'] &&
|
|
||||||
params['symbol']
|
|
||||||
) {
|
|
||||||
this.openPositionDialog({
|
|
||||||
dataSource: params['dataSource'],
|
|
||||||
symbol: params['symbol']
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -152,32 +142,24 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
|||||||
this.openCreateActivityDialog(aActivity);
|
this.openCreateActivityDialog(aActivity);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onDeleteActivity(aId: string) {
|
public onDeleteActivities() {
|
||||||
this.dataService
|
this.dataService
|
||||||
.deleteOrder(aId)
|
.deleteActivities({
|
||||||
|
filters: this.userService.getFilters()
|
||||||
|
})
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe({
|
.subscribe(() => {
|
||||||
next: () => {
|
this.fetchActivities();
|
||||||
this.fetchActivities();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onDeleteAllActivities() {
|
public onDeleteActivity(aId: string) {
|
||||||
const confirmation = confirm(
|
this.dataService
|
||||||
$localize`Do you really want to delete all your activities?`
|
.deleteActivity(aId)
|
||||||
);
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {
|
||||||
if (confirmation) {
|
this.fetchActivities();
|
||||||
this.dataService
|
});
|
||||||
.deleteAllOrders()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe({
|
|
||||||
next: () => {
|
|
||||||
this.fetchActivities();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public onExport(activityIds?: string[]) {
|
public onExport(activityIds?: string[]) {
|
||||||
@ -287,9 +269,7 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
|||||||
dialogRef
|
dialogRef
|
||||||
.afterClosed()
|
.afterClosed()
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((data: any) => {
|
.subscribe((transaction: UpdateOrderDto | null) => {
|
||||||
const transaction: UpdateOrderDto = data?.activity;
|
|
||||||
|
|
||||||
if (transaction) {
|
if (transaction) {
|
||||||
this.dataService
|
this.dataService
|
||||||
.putOrder(transaction)
|
.putOrder(transaction)
|
||||||
@ -338,9 +318,7 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
|||||||
dialogRef
|
dialogRef
|
||||||
.afterClosed()
|
.afterClosed()
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((data: any) => {
|
.subscribe((transaction: CreateOrderDto | null) => {
|
||||||
const transaction: CreateOrderDto = data?.activity;
|
|
||||||
|
|
||||||
if (transaction) {
|
if (transaction) {
|
||||||
this.dataService.postOrder(transaction).subscribe({
|
this.dataService.postOrder(transaction).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
@ -354,47 +332,6 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private openPositionDialog({
|
|
||||||
dataSource,
|
|
||||||
symbol
|
|
||||||
}: {
|
|
||||||
dataSource: DataSource;
|
|
||||||
symbol: string;
|
|
||||||
}) {
|
|
||||||
this.userService
|
|
||||||
.get()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe((user) => {
|
|
||||||
this.updateUser(user);
|
|
||||||
|
|
||||||
const dialogRef = this.dialog.open(PositionDetailDialog, {
|
|
||||||
autoFocus: false,
|
|
||||||
data: <PositionDetailDialogParams>{
|
|
||||||
dataSource,
|
|
||||||
symbol,
|
|
||||||
baseCurrency: this.user?.settings?.baseCurrency,
|
|
||||||
colorScheme: this.user?.settings?.colorScheme,
|
|
||||||
deviceType: this.deviceType,
|
|
||||||
hasImpersonationId: this.hasImpersonationId,
|
|
||||||
hasPermissionToReportDataGlitch: hasPermission(
|
|
||||||
this.user?.permissions,
|
|
||||||
permissions.reportDataGlitch
|
|
||||||
),
|
|
||||||
locale: this.user?.settings?.locale
|
|
||||||
},
|
|
||||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
|
||||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
|
||||||
});
|
|
||||||
|
|
||||||
dialogRef
|
|
||||||
.afterClosed()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe(() => {
|
|
||||||
this.router.navigate(['.'], { relativeTo: this.route });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateUser(aUser: User) {
|
private updateUser(aUser: User) {
|
||||||
this.user = aUser;
|
this.user = aUser;
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
[dataSource]="dataSource"
|
[dataSource]="dataSource"
|
||||||
[deviceType]="deviceType"
|
[deviceType]="deviceType"
|
||||||
[hasPermissionToCreateActivity]="hasPermissionToCreateActivity"
|
[hasPermissionToCreateActivity]="hasPermissionToCreateActivity"
|
||||||
|
[hasPermissionToDeleteActivity]="hasPermissionToDeleteActivity"
|
||||||
[hasPermissionToExportActivities]="!hasImpersonationId"
|
[hasPermissionToExportActivities]="!hasImpersonationId"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[pageIndex]="pageIndex"
|
[pageIndex]="pageIndex"
|
||||||
@ -19,10 +20,10 @@
|
|||||||
[sortColumn]="sortColumn"
|
[sortColumn]="sortColumn"
|
||||||
[sortDirection]="sortDirection"
|
[sortDirection]="sortDirection"
|
||||||
[totalItems]="totalItems"
|
[totalItems]="totalItems"
|
||||||
|
(activitiesDeleted)="onDeleteActivities()"
|
||||||
(activityDeleted)="onDeleteActivity($event)"
|
(activityDeleted)="onDeleteActivity($event)"
|
||||||
(activityToClone)="onCloneActivity($event)"
|
(activityToClone)="onCloneActivity($event)"
|
||||||
(activityToUpdate)="onUpdateActivity($event)"
|
(activityToUpdate)="onUpdateActivity($event)"
|
||||||
(deleteAllActivities)="onDeleteAllActivities()"
|
|
||||||
(export)="onExport()"
|
(export)="onExport()"
|
||||||
(exportDrafts)="onExportDrafts($event)"
|
(exportDrafts)="onExportDrafts($event)"
|
||||||
(import)="onImport()"
|
(import)="onImport()"
|
||||||
|
@ -475,6 +475,8 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
|||||||
ignoreFields: ['dataSource', 'date'],
|
ignoreFields: ['dataSource', 'date'],
|
||||||
object: activity as UpdateOrderDto
|
object: activity as UpdateOrderDto
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.dialogRef.close(activity as UpdateOrderDto);
|
||||||
} else {
|
} else {
|
||||||
(activity as CreateOrderDto).updateAccountBalance =
|
(activity as CreateOrderDto).updateAccountBalance =
|
||||||
this.activityForm.get('updateAccountBalance').value;
|
this.activityForm.get('updateAccountBalance').value;
|
||||||
@ -485,9 +487,9 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
|||||||
ignoreFields: ['dataSource', 'date'],
|
ignoreFields: ['dataSource', 'date'],
|
||||||
object: activity
|
object: activity
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
this.dialogRef.close({ activity });
|
this.dialogRef.close(activity as CreateOrderDto);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto
|
|||||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { ImportActivitiesService } from '@ghostfolio/client/services/import-activities.service';
|
import { ImportActivitiesService } from '@ghostfolio/client/services/import-activities.service';
|
||||||
import { Position } from '@ghostfolio/common/interfaces';
|
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
StepperOrientation,
|
StepperOrientation,
|
||||||
@ -43,7 +43,7 @@ export class ImportActivitiesDialog implements OnDestroy {
|
|||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
public dialogTitle = $localize`Import Activities`;
|
public dialogTitle = $localize`Import Activities`;
|
||||||
public errorMessages: string[] = [];
|
public errorMessages: string[] = [];
|
||||||
public holdings: Position[] = [];
|
public holdings: PortfolioPosition[] = [];
|
||||||
public importStep: ImportStep = ImportStep.UPLOAD_FILE;
|
public importStep: ImportStep = ImportStep.UPLOAD_FILE;
|
||||||
public isLoading = false;
|
public isLoading = false;
|
||||||
public maxSafeInteger = Number.MAX_SAFE_INTEGER;
|
public maxSafeInteger = Number.MAX_SAFE_INTEGER;
|
||||||
@ -88,7 +88,7 @@ export class ImportActivitiesDialog implements OnDestroy {
|
|||||||
this.uniqueAssetForm.get('uniqueAsset').disable();
|
this.uniqueAssetForm.get('uniqueAsset').disable();
|
||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchPositions({
|
.fetchPortfolioHoldings({
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
id: AssetClass.EQUITY,
|
id: AssetClass.EQUITY,
|
||||||
@ -98,8 +98,8 @@ export class ImportActivitiesDialog implements OnDestroy {
|
|||||||
range: 'max'
|
range: 'max'
|
||||||
})
|
})
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(({ positions }) => {
|
.subscribe(({ holdings }) => {
|
||||||
this.holdings = sortBy(positions, ({ name }) => {
|
this.holdings = sortBy(holdings, ({ name }) => {
|
||||||
return name.toLowerCase();
|
return name.toLowerCase();
|
||||||
});
|
});
|
||||||
this.uniqueAssetForm.get('uniqueAsset').enable();
|
this.uniqueAssetForm.get('uniqueAsset').enable();
|
||||||
|
@ -126,6 +126,7 @@
|
|||||||
[dataSource]="dataSource"
|
[dataSource]="dataSource"
|
||||||
[deviceType]="data?.deviceType"
|
[deviceType]="data?.deviceType"
|
||||||
[hasPermissionToCreateActivity]="false"
|
[hasPermissionToCreateActivity]="false"
|
||||||
|
[hasPermissionToDeleteActivity]="false"
|
||||||
[hasPermissionToExportActivities]="false"
|
[hasPermissionToExportActivities]="false"
|
||||||
[hasPermissionToFilter]="false"
|
[hasPermissionToFilter]="false"
|
||||||
[hasPermissionToOpenDetails]="false"
|
[hasPermissionToOpenDetails]="false"
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import { AccountDetailDialog } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.component';
|
import { AccountDetailDialog } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.component';
|
||||||
import { AccountDetailDialogParams } from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces';
|
import { AccountDetailDialogParams } from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces';
|
||||||
import { PositionDetailDialogParams } from '@ghostfolio/client/components/position/position-detail-dialog/interfaces/interfaces';
|
|
||||||
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-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';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
@ -13,7 +11,6 @@ import {
|
|||||||
UniqueAsset,
|
UniqueAsset,
|
||||||
User
|
User
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
|
||||||
import { Market, MarketAdvanced } from '@ghostfolio/common/types';
|
import { Market, MarketAdvanced } from '@ghostfolio/common/types';
|
||||||
import { translate } from '@ghostfolio/ui/i18n';
|
import { translate } from '@ghostfolio/ui/i18n';
|
||||||
|
|
||||||
@ -103,20 +100,11 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
private router: Router,
|
private router: Router,
|
||||||
private userService: UserService
|
private userService: UserService
|
||||||
) {
|
) {
|
||||||
route.queryParams
|
this.route.queryParams
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((params) => {
|
.subscribe((params) => {
|
||||||
if (params['accountId'] && params['accountDetailDialog']) {
|
if (params['accountId'] && params['accountDetailDialog']) {
|
||||||
this.openAccountDetailDialog(params['accountId']);
|
this.openAccountDetailDialog(params['accountId']);
|
||||||
} else if (
|
|
||||||
params['dataSource'] &&
|
|
||||||
params['positionDetailDialog'] &&
|
|
||||||
params['symbol']
|
|
||||||
) {
|
|
||||||
this.openPositionDialog({
|
|
||||||
dataSource: params['dataSource'],
|
|
||||||
symbol: params['symbol']
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -178,7 +166,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
public onSymbolChartClicked({ dataSource, symbol }: UniqueAsset) {
|
public onSymbolChartClicked({ dataSource, symbol }: UniqueAsset) {
|
||||||
if (dataSource && symbol) {
|
if (dataSource && symbol) {
|
||||||
this.router.navigate([], {
|
this.router.navigate([], {
|
||||||
queryParams: { dataSource, symbol, positionDetailDialog: true }
|
queryParams: { dataSource, symbol, holdingDetailDialog: true }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -551,45 +539,4 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
this.router.navigate(['.'], { relativeTo: this.route });
|
this.router.navigate(['.'], { relativeTo: this.route });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private openPositionDialog({
|
|
||||||
dataSource,
|
|
||||||
symbol
|
|
||||||
}: {
|
|
||||||
dataSource: DataSource;
|
|
||||||
symbol: string;
|
|
||||||
}) {
|
|
||||||
this.userService
|
|
||||||
.get()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe((user) => {
|
|
||||||
this.user = user;
|
|
||||||
|
|
||||||
const dialogRef = this.dialog.open(PositionDetailDialog, {
|
|
||||||
autoFocus: false,
|
|
||||||
data: <PositionDetailDialogParams>{
|
|
||||||
dataSource,
|
|
||||||
symbol,
|
|
||||||
baseCurrency: this.user?.settings?.baseCurrency,
|
|
||||||
colorScheme: this.user?.settings?.colorScheme,
|
|
||||||
deviceType: this.deviceType,
|
|
||||||
hasImpersonationId: this.hasImpersonationId,
|
|
||||||
hasPermissionToReportDataGlitch: hasPermission(
|
|
||||||
this.user?.permissions,
|
|
||||||
permissions.reportDataGlitch
|
|
||||||
),
|
|
||||||
locale: this.user?.settings?.locale
|
|
||||||
},
|
|
||||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
|
||||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
|
||||||
});
|
|
||||||
|
|
||||||
dialogRef
|
|
||||||
.afterClosed()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe(() => {
|
|
||||||
this.router.navigate(['.'], { relativeTo: this.route });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import { PositionDetailDialogParams } from '@ghostfolio/client/components/position/position-detail-dialog/interfaces/interfaces';
|
|
||||||
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
|
|
||||||
import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component';
|
import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.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';
|
||||||
@ -8,18 +6,15 @@ import {
|
|||||||
HistoricalDataItem,
|
HistoricalDataItem,
|
||||||
PortfolioInvestments,
|
PortfolioInvestments,
|
||||||
PortfolioPerformance,
|
PortfolioPerformance,
|
||||||
Position,
|
PortfolioPosition,
|
||||||
User
|
User
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
|
||||||
import { GroupBy, ToggleOption } from '@ghostfolio/common/types';
|
import { GroupBy, ToggleOption } from '@ghostfolio/common/types';
|
||||||
import { translate } from '@ghostfolio/ui/i18n';
|
import { translate } from '@ghostfolio/ui/i18n';
|
||||||
|
|
||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { SymbolProfile } from '@prisma/client';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
|
||||||
import { DataSource, SymbolProfile } from '@prisma/client';
|
|
||||||
import { differenceInDays } from 'date-fns';
|
import { differenceInDays } from 'date-fns';
|
||||||
import { isNumber, sortBy } from 'lodash';
|
import { isNumber, sortBy } from 'lodash';
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
@ -35,7 +30,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
public benchmark: Partial<SymbolProfile>;
|
public benchmark: Partial<SymbolProfile>;
|
||||||
public benchmarkDataItems: HistoricalDataItem[] = [];
|
public benchmarkDataItems: HistoricalDataItem[] = [];
|
||||||
public benchmarks: Partial<SymbolProfile>[];
|
public benchmarks: Partial<SymbolProfile>[];
|
||||||
public bottom3: Position[];
|
public bottom3: PortfolioPosition[];
|
||||||
public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS;
|
public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS;
|
||||||
public daysInMarket: number;
|
public daysInMarket: number;
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
@ -60,7 +55,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
public performanceDataItemsInPercentage: HistoricalDataItem[];
|
public performanceDataItemsInPercentage: HistoricalDataItem[];
|
||||||
public portfolioEvolutionDataLabel = $localize`Investment`;
|
public portfolioEvolutionDataLabel = $localize`Investment`;
|
||||||
public streaks: PortfolioInvestments['streaks'];
|
public streaks: PortfolioInvestments['streaks'];
|
||||||
public top3: Position[];
|
public top3: PortfolioPosition[];
|
||||||
public unitCurrentStreak: string;
|
public unitCurrentStreak: string;
|
||||||
public unitLongestStreak: string;
|
public unitLongestStreak: string;
|
||||||
public user: User;
|
public user: User;
|
||||||
@ -70,30 +65,12 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
public constructor(
|
public constructor(
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
private dialog: MatDialog,
|
|
||||||
private deviceService: DeviceDetectorService,
|
private deviceService: DeviceDetectorService,
|
||||||
private impersonationStorageService: ImpersonationStorageService,
|
private impersonationStorageService: ImpersonationStorageService,
|
||||||
private route: ActivatedRoute,
|
|
||||||
private router: Router,
|
|
||||||
private userService: UserService
|
private userService: UserService
|
||||||
) {
|
) {
|
||||||
const { benchmarks } = this.dataService.fetchInfo();
|
const { benchmarks } = this.dataService.fetchInfo();
|
||||||
this.benchmarks = benchmarks;
|
this.benchmarks = benchmarks;
|
||||||
|
|
||||||
route.queryParams
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe((params) => {
|
|
||||||
if (
|
|
||||||
params['dataSource'] &&
|
|
||||||
params['positionDetailDialog'] &&
|
|
||||||
params['symbol']
|
|
||||||
) {
|
|
||||||
this.openPositionDialog({
|
|
||||||
dataSource: params['dataSource'],
|
|
||||||
symbol: params['symbol']
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get savingsRate() {
|
get savingsRate() {
|
||||||
@ -212,47 +189,6 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private openPositionDialog({
|
|
||||||
dataSource,
|
|
||||||
symbol
|
|
||||||
}: {
|
|
||||||
dataSource: DataSource;
|
|
||||||
symbol: string;
|
|
||||||
}) {
|
|
||||||
this.userService
|
|
||||||
.get()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe((user) => {
|
|
||||||
this.user = user;
|
|
||||||
|
|
||||||
const dialogRef = this.dialog.open(PositionDetailDialog, {
|
|
||||||
autoFocus: false,
|
|
||||||
data: <PositionDetailDialogParams>{
|
|
||||||
dataSource,
|
|
||||||
symbol,
|
|
||||||
baseCurrency: this.user?.settings?.baseCurrency,
|
|
||||||
colorScheme: this.user?.settings?.colorScheme,
|
|
||||||
deviceType: this.deviceType,
|
|
||||||
hasImpersonationId: this.hasImpersonationId,
|
|
||||||
hasPermissionToReportDataGlitch: hasPermission(
|
|
||||||
this.user?.permissions,
|
|
||||||
permissions.reportDataGlitch
|
|
||||||
),
|
|
||||||
locale: this.user?.settings?.locale
|
|
||||||
},
|
|
||||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
|
||||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
|
||||||
});
|
|
||||||
|
|
||||||
dialogRef
|
|
||||||
.afterClosed()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe(() => {
|
|
||||||
this.router.navigate(['.'], { relativeTo: this.route });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private update() {
|
private update() {
|
||||||
this.isLoadingInvestmentChart = true;
|
this.isLoadingInvestmentChart = true;
|
||||||
|
|
||||||
@ -308,23 +244,23 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchPositions({
|
.fetchPortfolioHoldings({
|
||||||
filters: this.userService.getFilters(),
|
filters: this.userService.getFilters(),
|
||||||
range: this.user?.settings?.dateRange
|
range: this.user?.settings?.dateRange
|
||||||
})
|
})
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(({ positions }) => {
|
.subscribe(({ holdings }) => {
|
||||||
const positionsSorted = sortBy(
|
const holdingsSorted = sortBy(
|
||||||
positions.filter(({ netPerformancePercentageWithCurrencyEffect }) => {
|
holdings.filter(({ netPerformancePercentWithCurrencyEffect }) => {
|
||||||
return isNumber(netPerformancePercentageWithCurrencyEffect);
|
return isNumber(netPerformancePercentWithCurrencyEffect);
|
||||||
}),
|
}),
|
||||||
'netPerformancePercentageWithCurrencyEffect'
|
'netPerformancePercentWithCurrencyEffect'
|
||||||
).reverse();
|
).reverse();
|
||||||
|
|
||||||
this.top3 = positionsSorted.slice(0, 3);
|
this.top3 = holdingsSorted.slice(0, 3);
|
||||||
|
|
||||||
if (positions?.length > 3) {
|
if (holdings?.length > 3) {
|
||||||
this.bottom3 = positionsSorted.slice(-3).reverse();
|
this.bottom3 = holdingsSorted.slice(-3).reverse();
|
||||||
} else {
|
} else {
|
||||||
this.bottom3 = [];
|
this.bottom3 = [];
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@
|
|||||||
[value]="
|
[value]="
|
||||||
isLoadingInvestmentChart
|
isLoadingInvestmentChart
|
||||||
? undefined
|
? undefined
|
||||||
: performance?.currentNetPerformance
|
: performance?.netPerformance
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -61,7 +61,7 @@
|
|||||||
[value]="
|
[value]="
|
||||||
isLoadingInvestmentChart
|
isLoadingInvestmentChart
|
||||||
? undefined
|
? undefined
|
||||||
: performance?.currentNetPerformancePercent
|
: performance?.netPerformancePercentage
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -86,10 +86,10 @@
|
|||||||
[value]="
|
[value]="
|
||||||
isLoadingInvestmentChart
|
isLoadingInvestmentChart
|
||||||
? undefined
|
? undefined
|
||||||
: performance?.currentNetPerformance === null
|
: performance?.netPerformance === null
|
||||||
? null
|
? null
|
||||||
: performance?.currentNetPerformanceWithCurrencyEffect -
|
: performance?.netPerformanceWithCurrencyEffect -
|
||||||
performance?.currentNetPerformance
|
performance?.netPerformance
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -108,10 +108,10 @@
|
|||||||
[value]="
|
[value]="
|
||||||
isLoadingInvestmentChart
|
isLoadingInvestmentChart
|
||||||
? undefined
|
? undefined
|
||||||
: performance?.currentNetPerformancePercent === null
|
: performance?.netPerformancePercentage === null
|
||||||
? null
|
? null
|
||||||
: performance?.currentNetPerformancePercentWithCurrencyEffect -
|
: performance?.netPerformancePercentageWithCurrencyEffect -
|
||||||
performance?.currentNetPerformancePercent
|
performance?.netPerformancePercentage
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -131,7 +131,7 @@
|
|||||||
[value]="
|
[value]="
|
||||||
isLoadingInvestmentChart
|
isLoadingInvestmentChart
|
||||||
? undefined
|
? undefined
|
||||||
: performance?.currentNetPerformanceWithCurrencyEffect
|
: performance?.netPerformanceWithCurrencyEffect
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -150,7 +150,7 @@
|
|||||||
[value]="
|
[value]="
|
||||||
isLoadingInvestmentChart
|
isLoadingInvestmentChart
|
||||||
? undefined
|
? undefined
|
||||||
: performance?.currentNetPerformancePercentWithCurrencyEffect
|
: performance?.netPerformancePercentageWithCurrencyEffect
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -170,17 +170,17 @@
|
|||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<ol class="mb-0 ml-1 pl-3">
|
<ol class="mb-0 ml-1 pl-3">
|
||||||
<li *ngFor="let position of top3" class="py-1">
|
<li *ngFor="let holding of top3" class="py-1">
|
||||||
<a
|
<a
|
||||||
class="d-flex"
|
class="d-flex"
|
||||||
[queryParams]="{
|
[queryParams]="{
|
||||||
dataSource: position.dataSource,
|
dataSource: holding.dataSource,
|
||||||
positionDetailDialog: true,
|
holdingDetailDialog: true,
|
||||||
symbol: position.symbol
|
symbol: holding.symbol
|
||||||
}"
|
}"
|
||||||
[routerLink]="[]"
|
[routerLink]="[]"
|
||||||
>
|
>
|
||||||
<div class="flex-grow-1 mr-2">{{ position.name }}</div>
|
<div class="flex-grow-1 mr-2">{{ holding.name }}</div>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<gf-value
|
<gf-value
|
||||||
class="justify-content-end"
|
class="justify-content-end"
|
||||||
@ -188,9 +188,7 @@
|
|||||||
[colorizeSign]="true"
|
[colorizeSign]="true"
|
||||||
[isPercent]="true"
|
[isPercent]="true"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[value]="
|
[value]="holding.netPerformancePercentWithCurrencyEffect"
|
||||||
position.netPerformancePercentageWithCurrencyEffect
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@ -218,17 +216,17 @@
|
|||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<ol class="mb-0 ml-1 pl-3">
|
<ol class="mb-0 ml-1 pl-3">
|
||||||
<li *ngFor="let position of bottom3" class="py-1">
|
<li *ngFor="let holding of bottom3" class="py-1">
|
||||||
<a
|
<a
|
||||||
class="d-flex"
|
class="d-flex"
|
||||||
[queryParams]="{
|
[queryParams]="{
|
||||||
dataSource: position.dataSource,
|
dataSource: holding.dataSource,
|
||||||
positionDetailDialog: true,
|
holdingDetailDialog: true,
|
||||||
symbol: position.symbol
|
symbol: holding.symbol
|
||||||
}"
|
}"
|
||||||
[routerLink]="[]"
|
[routerLink]="[]"
|
||||||
>
|
>
|
||||||
<div class="flex-grow-1 mr-2">{{ position.name }}</div>
|
<div class="flex-grow-1 mr-2">{{ holding.name }}</div>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<gf-value
|
<gf-value
|
||||||
class="justify-content-end"
|
class="justify-content-end"
|
||||||
@ -236,9 +234,7 @@
|
|||||||
[colorizeSign]="true"
|
[colorizeSign]="true"
|
||||||
[isPercent]="true"
|
[isPercent]="true"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[value]="
|
[value]="holding.netPerformancePercentWithCurrencyEffect"
|
||||||
position.netPerformancePercentageWithCurrencyEffect
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
|
||||||
|
|
||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
|
||||||
|
|
||||||
import { HoldingsPageComponent } from './holdings-page.component';
|
|
||||||
|
|
||||||
const routes: Routes = [
|
|
||||||
{
|
|
||||||
canActivate: [AuthGuard],
|
|
||||||
component: HoldingsPageComponent,
|
|
||||||
path: '',
|
|
||||||
title: $localize`Holdings`
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [RouterModule.forChild(routes)],
|
|
||||||
exports: [RouterModule]
|
|
||||||
})
|
|
||||||
export class HoldingsPageRoutingModule {}
|
|
@ -1,171 +0,0 @@
|
|||||||
import { PositionDetailDialogParams } from '@ghostfolio/client/components/position/position-detail-dialog/interfaces/interfaces';
|
|
||||||
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
|
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
|
||||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
|
||||||
import { PortfolioPosition, User } from '@ghostfolio/common/interfaces';
|
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
|
||||||
import { HoldingType, ToggleOption } from '@ghostfolio/common/types';
|
|
||||||
|
|
||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
|
||||||
import { DataSource } from '@prisma/client';
|
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
|
||||||
import { Subject } from 'rxjs';
|
|
||||||
import { takeUntil } from 'rxjs/operators';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'gf-holdings-page',
|
|
||||||
styleUrls: ['./holdings-page.scss'],
|
|
||||||
templateUrl: './holdings-page.html'
|
|
||||||
})
|
|
||||||
export class HoldingsPageComponent implements OnDestroy, OnInit {
|
|
||||||
public deviceType: string;
|
|
||||||
public hasImpersonationId: boolean;
|
|
||||||
public hasPermissionToCreateOrder: boolean;
|
|
||||||
public holdings: PortfolioPosition[];
|
|
||||||
public holdingType: HoldingType = 'ACTIVE';
|
|
||||||
public holdingTypeOptions: ToggleOption[] = [
|
|
||||||
{ label: $localize`Active`, value: 'ACTIVE' },
|
|
||||||
{ label: $localize`Closed`, value: 'CLOSED' }
|
|
||||||
];
|
|
||||||
public user: User;
|
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
|
||||||
|
|
||||||
public constructor(
|
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
|
||||||
private dataService: DataService,
|
|
||||||
private deviceService: DeviceDetectorService,
|
|
||||||
private dialog: MatDialog,
|
|
||||||
private impersonationStorageService: ImpersonationStorageService,
|
|
||||||
private route: ActivatedRoute,
|
|
||||||
private router: Router,
|
|
||||||
private userService: UserService
|
|
||||||
) {
|
|
||||||
route.queryParams
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe((params) => {
|
|
||||||
if (
|
|
||||||
params['dataSource'] &&
|
|
||||||
params['positionDetailDialog'] &&
|
|
||||||
params['symbol']
|
|
||||||
) {
|
|
||||||
this.openPositionDialog({
|
|
||||||
dataSource: params['dataSource'],
|
|
||||||
symbol: params['symbol']
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public ngOnInit() {
|
|
||||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
|
||||||
|
|
||||||
this.impersonationStorageService
|
|
||||||
.onChangeHasImpersonation()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe((impersonationId) => {
|
|
||||||
this.hasImpersonationId = !!impersonationId;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.userService.stateChanged
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe((state) => {
|
|
||||||
if (state?.user) {
|
|
||||||
this.user = state.user;
|
|
||||||
|
|
||||||
this.hasPermissionToCreateOrder = hasPermission(
|
|
||||||
this.user.permissions,
|
|
||||||
permissions.createOrder
|
|
||||||
);
|
|
||||||
|
|
||||||
this.holdings = undefined;
|
|
||||||
|
|
||||||
this.fetchHoldings()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe(({ holdings }) => {
|
|
||||||
this.holdings = holdings;
|
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public onChangeHoldingType(aHoldingType: HoldingType) {
|
|
||||||
this.holdingType = aHoldingType;
|
|
||||||
|
|
||||||
this.holdings = undefined;
|
|
||||||
|
|
||||||
this.fetchHoldings()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe(({ holdings }) => {
|
|
||||||
this.holdings = holdings;
|
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public ngOnDestroy() {
|
|
||||||
this.unsubscribeSubject.next();
|
|
||||||
this.unsubscribeSubject.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
private fetchHoldings() {
|
|
||||||
const filters = this.userService.getFilters();
|
|
||||||
|
|
||||||
if (this.holdingType === 'CLOSED') {
|
|
||||||
filters.push({ id: 'CLOSED', type: 'HOLDING_TYPE' });
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.dataService.fetchPortfolioHoldings({
|
|
||||||
filters,
|
|
||||||
range: this.user?.settings?.dateRange
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private openPositionDialog({
|
|
||||||
dataSource,
|
|
||||||
symbol
|
|
||||||
}: {
|
|
||||||
dataSource: DataSource;
|
|
||||||
symbol: string;
|
|
||||||
}) {
|
|
||||||
this.userService
|
|
||||||
.get()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe((user) => {
|
|
||||||
this.user = user;
|
|
||||||
|
|
||||||
const dialogRef = this.dialog.open(PositionDetailDialog, {
|
|
||||||
autoFocus: false,
|
|
||||||
data: <PositionDetailDialogParams>{
|
|
||||||
dataSource,
|
|
||||||
symbol,
|
|
||||||
baseCurrency: this.user?.settings?.baseCurrency,
|
|
||||||
colorScheme: this.user?.settings?.colorScheme,
|
|
||||||
deviceType: this.deviceType,
|
|
||||||
hasImpersonationId: this.hasImpersonationId,
|
|
||||||
hasPermissionToReportDataGlitch: hasPermission(
|
|
||||||
this.user?.permissions,
|
|
||||||
permissions.reportDataGlitch
|
|
||||||
),
|
|
||||||
locale: this.user?.settings?.locale
|
|
||||||
},
|
|
||||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
|
||||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
|
||||||
});
|
|
||||||
|
|
||||||
dialogRef
|
|
||||||
.afterClosed()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe(() => {
|
|
||||||
this.router.navigate(['.'], { relativeTo: this.route });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Holdings</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg">
|
|
||||||
<div class="d-flex justify-content-end">
|
|
||||||
<gf-toggle
|
|
||||||
class="d-none d-lg-block"
|
|
||||||
[defaultValue]="holdingType"
|
|
||||||
[isLoading]="false"
|
|
||||||
[options]="holdingTypeOptions"
|
|
||||||
(change)="onChangeHoldingType($event.value)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<gf-holdings-table
|
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
|
||||||
[deviceType]="deviceType"
|
|
||||||
[hasPermissionToCreateActivity]="hasPermissionToCreateOrder"
|
|
||||||
[holdings]="holdings"
|
|
||||||
[locale]="user?.settings?.locale"
|
|
||||||
/>
|
|
||||||
@if (hasPermissionToCreateOrder && holdings?.length > 0) {
|
|
||||||
<div class="text-center">
|
|
||||||
<a
|
|
||||||
class="mt-3"
|
|
||||||
i18n
|
|
||||||
mat-stroked-button
|
|
||||||
[routerLink]="['/portfolio', 'activities']"
|
|
||||||
>Manage Activities</a
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,22 +0,0 @@
|
|||||||
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
|
||||||
import { GfHoldingsTableComponent } from '@ghostfolio/ui/holdings-table';
|
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
|
||||||
|
|
||||||
import { HoldingsPageRoutingModule } from './holdings-page-routing.module';
|
|
||||||
import { HoldingsPageComponent } from './holdings-page.component';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [HoldingsPageComponent],
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
GfHoldingsTableComponent,
|
|
||||||
GfToggleModule,
|
|
||||||
HoldingsPageRoutingModule,
|
|
||||||
MatButtonModule
|
|
||||||
],
|
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
|
||||||
})
|
|
||||||
export class HoldingsPageModule {}
|
|
@ -1,3 +0,0 @@
|
|||||||
:host {
|
|
||||||
display: block;
|
|
||||||
}
|
|
@ -16,13 +16,6 @@ const routes: Routes = [
|
|||||||
(m) => m.AnalysisPageModule
|
(m) => m.AnalysisPageModule
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'holdings',
|
|
||||||
loadChildren: () =>
|
|
||||||
import('./holdings/holdings-page.module').then(
|
|
||||||
(m) => m.HoldingsPageModule
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'activities',
|
path: 'activities',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
|
@ -34,11 +34,6 @@ export class PortfolioPageComponent implements OnDestroy, OnInit {
|
|||||||
label: $localize`Analysis`,
|
label: $localize`Analysis`,
|
||||||
path: ['/portfolio']
|
path: ['/portfolio']
|
||||||
},
|
},
|
||||||
{
|
|
||||||
iconName: 'wallet-outline',
|
|
||||||
label: $localize`Holdings`,
|
|
||||||
path: ['/portfolio', 'holdings']
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
iconName: 'swap-vertical-outline',
|
iconName: 'swap-vertical-outline',
|
||||||
label: $localize`Activities`,
|
label: $localize`Activities`,
|
||||||
|
@ -66,7 +66,8 @@ export const products: Product[] = [
|
|||||||
'Français',
|
'Français',
|
||||||
'Italiano',
|
'Italiano',
|
||||||
'Nederlands',
|
'Nederlands',
|
||||||
'Português'
|
'Português',
|
||||||
|
'Türkçe'
|
||||||
],
|
],
|
||||||
name: 'Ghostfolio',
|
name: 'Ghostfolio',
|
||||||
origin: $localize`Switzerland`,
|
origin: $localize`Switzerland`,
|
||||||
|
@ -6,7 +6,6 @@ import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
|||||||
import { Activities } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
import { Activities } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||||
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
||||||
import { PortfolioPositionDetail } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position-detail.interface';
|
import { PortfolioPositionDetail } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position-detail.interface';
|
||||||
import { PortfolioPositions } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-positions.interface';
|
|
||||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||||
import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface';
|
import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface';
|
||||||
import { UserItem } from '@ghostfolio/api/app/user/interfaces/user-item.interface';
|
import { UserItem } from '@ghostfolio/api/app/user/interfaces/user-item.interface';
|
||||||
@ -257,18 +256,20 @@ export class DataService {
|
|||||||
return this.http.delete<any>(`/api/v1/account-balance/${aId}`);
|
return this.http.delete<any>(`/api/v1/account-balance/${aId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public deleteAllOrders() {
|
public deleteActivities({ filters }) {
|
||||||
return this.http.delete<any>(`/api/v1/order/`);
|
let params = this.buildFiltersAsQueryParams({ filters });
|
||||||
|
|
||||||
|
return this.http.delete<any>(`/api/v1/order`, { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
public deleteActivity(aId: string) {
|
||||||
|
return this.http.delete<any>(`/api/v1/order/${aId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public deleteBenchmark({ dataSource, symbol }: UniqueAsset) {
|
public deleteBenchmark({ dataSource, symbol }: UniqueAsset) {
|
||||||
return this.http.delete<any>(`/api/v1/benchmark/${dataSource}/${symbol}`);
|
return this.http.delete<any>(`/api/v1/benchmark/${dataSource}/${symbol}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public deleteOrder(aId: string) {
|
|
||||||
return this.http.delete<any>(`/api/v1/order/${aId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
public deleteUser(aId: string) {
|
public deleteUser(aId: string) {
|
||||||
return this.http.delete<any>(`/api/v1/user/${aId}`);
|
return this.http.delete<any>(`/api/v1/user/${aId}`);
|
||||||
}
|
}
|
||||||
@ -376,21 +377,6 @@ export class DataService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchPositions({
|
|
||||||
filters,
|
|
||||||
range
|
|
||||||
}: {
|
|
||||||
filters?: Filter[];
|
|
||||||
range: DateRange;
|
|
||||||
}): Observable<PortfolioPositions> {
|
|
||||||
let params = this.buildFiltersAsQueryParams({ filters });
|
|
||||||
params = params.append('range', range);
|
|
||||||
|
|
||||||
return this.http.get<PortfolioPositions>('/api/v1/portfolio/positions', {
|
|
||||||
params
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public fetchSymbols({
|
public fetchSymbols({
|
||||||
includeIndices = false,
|
includeIndices = false,
|
||||||
query
|
query
|
||||||
|
@ -51,21 +51,21 @@ export class UserService extends ObservableStore<UserStoreState> {
|
|||||||
const filters: Filter[] = [];
|
const filters: Filter[] = [];
|
||||||
const user = this.getState().user;
|
const user = this.getState().user;
|
||||||
|
|
||||||
if (user.settings['filters.accounts']) {
|
if (user?.settings['filters.accounts']) {
|
||||||
filters.push({
|
filters.push({
|
||||||
id: user.settings['filters.accounts'][0],
|
id: user.settings['filters.accounts'][0],
|
||||||
type: 'ACCOUNT'
|
type: 'ACCOUNT'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.settings['filters.assetClasses']) {
|
if (user?.settings['filters.assetClasses']) {
|
||||||
filters.push({
|
filters.push({
|
||||||
id: user.settings['filters.assetClasses'][0],
|
id: user.settings['filters.assetClasses'][0],
|
||||||
type: 'ASSET_CLASS'
|
type: 'ASSET_CLASS'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.settings['filters.tags']) {
|
if (user?.settings['filters.tags']) {
|
||||||
filters.push({
|
filters.push({
|
||||||
id: user.settings['filters.tags'][0],
|
id: user.settings['filters.tags'][0],
|
||||||
type: 'TAG'
|
type: 'TAG'
|
||||||
@ -75,6 +75,10 @@ export class UserService extends ObservableStore<UserStoreState> {
|
|||||||
return filters;
|
return filters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public hasFilters() {
|
||||||
|
return this.getFilters().length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
public remove() {
|
public remove() {
|
||||||
this.setState({ user: null }, UserStoreActions.RemoveUser);
|
this.setState({ user: null }, UserStoreActions.RemoveUser);
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,14 @@ export async function validateObjectForForm<T>({
|
|||||||
validationError: Object.values(constraints)[0]
|
validationError: Object.values(constraints)[0]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formControlInCustomCurrency = form.get(`${property}InCustomCurrency`);
|
||||||
|
|
||||||
|
if (formControlInCustomCurrency) {
|
||||||
|
formControlInCustomCurrency.setErrors({
|
||||||
|
validationError: Object.values(constraints)[0]
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject(nonIgnoredErrors);
|
return Promise.reject(nonIgnoredErrors);
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user