Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
d9c07456cd | |||
0a53df4293 | |||
4416ba0c88 | |||
486de968a2 | |||
a5833566a8 | |||
261f5844dd | |||
2173c418a7 | |||
4efd5cefd8 | |||
d735e4db75 | |||
e10707fde4 |
22
CHANGELOG.md
22
CHANGELOG.md
@ -5,7 +5,27 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## 2.77.0 - 2024-04-27
|
||||
## 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
|
||||
|
||||
|
34
README.md
34
README.md
@ -85,23 +85,23 @@ We provide official container images hosted on [Docker Hub](https://hub.docker.c
|
||||
|
||||
### Supported Environment Variables
|
||||
|
||||
| Name | Default Value | Description |
|
||||
| ------------------------ | ------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `ACCESS_TOKEN_SALT` | | A random string used as salt for access tokens |
|
||||
| `API_KEY_COINGECKO_DEMO` | | The _CoinGecko_ Demo API key |
|
||||
| `API_KEY_COINGECKO_PRO` | | The _CoinGecko_ Pro API |
|
||||
| `DATABASE_URL` | | 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 |
|
||||
| `JWT_SECRET_KEY` | | A random string used for _JSON Web Tokens_ (JWT) |
|
||||
| `PORT` | `3333` | The port where the Ghostfolio application will run on |
|
||||
| `POSTGRES_DB` | | The name of the _PostgreSQL_ database |
|
||||
| `POSTGRES_PASSWORD` | | The password of the _PostgreSQL_ database |
|
||||
| `POSTGRES_USER` | | The user of the _PostgreSQL_ database |
|
||||
| `REDIS_DB` | `0` | The database index of _Redis_ |
|
||||
| `REDIS_HOST` | | The host where _Redis_ is running |
|
||||
| `REDIS_PASSWORD` | | The password of _Redis_ |
|
||||
| `REDIS_PORT` | | The port where _Redis_ is running |
|
||||
| `REQUEST_TIMEOUT` | `2000` | The timeout of network requests to data providers in milliseconds |
|
||||
| Name | Type | Default Value | Description |
|
||||
| ------------------------ | ------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `ACCESS_TOKEN_SALT` | string | | A random string used as salt for access tokens |
|
||||
| `API_KEY_COINGECKO_DEMO` | string (`optional`) | | The _CoinGecko_ Demo API key |
|
||||
| `API_KEY_COINGECKO_PRO` | string (`optional`) | | The _CoinGecko_ Pro API |
|
||||
| `DATABASE_URL` | string | | The database connection URL, e.g. `postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?sslmode=prefer` |
|
||||
| `HOST` | string (`optional`) | `0.0.0.0` | The host where the Ghostfolio application will run on |
|
||||
| `JWT_SECRET_KEY` | string | | A random string used for _JSON Web Tokens_ (JWT) |
|
||||
| `PORT` | number (`optional`) | `3333` | The port where the Ghostfolio application will run on |
|
||||
| `POSTGRES_DB` | string | | The name of the _PostgreSQL_ database |
|
||||
| `POSTGRES_PASSWORD` | string | | The password of the _PostgreSQL_ database |
|
||||
| `POSTGRES_USER` | string | | The user of the _PostgreSQL_ database |
|
||||
| `REDIS_DB` | number (`optional`) | `0` | The database index of _Redis_ |
|
||||
| `REDIS_HOST` | string | | The host where _Redis_ is running |
|
||||
| `REDIS_PASSWORD` | string | | The password of _Redis_ |
|
||||
| `REDIS_PORT` | number | | The port where _Redis_ is running |
|
||||
| `REQUEST_TIMEOUT` | number (`optional`) | `2000` | The timeout of network requests to data providers in milliseconds |
|
||||
|
||||
### Run with Docker Compose
|
||||
|
||||
|
@ -2,10 +2,12 @@ import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||
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 * as countriesAndTimezones from 'countries-and-timezones';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
|
||||
@Injectable()
|
||||
@ -29,6 +31,13 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
|
||||
if (user) {
|
||||
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 =
|
||||
countriesAndTimezones.getCountryForTimezone(timezone)?.id;
|
||||
|
||||
@ -45,10 +54,20 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
|
||||
return user;
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -88,21 +88,26 @@ export class OrderController {
|
||||
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId,
|
||||
@Query('accounts') filterByAccounts?: string,
|
||||
@Query('assetClasses') filterByAssetClasses?: string,
|
||||
@Query('range') dateRange: DateRange = 'max',
|
||||
@Query('range') dateRange?: DateRange,
|
||||
@Query('skip') skip?: number,
|
||||
@Query('sortColumn') sortColumn?: string,
|
||||
@Query('sortDirection') sortDirection?: Prisma.SortOrder,
|
||||
@Query('tags') filterByTags?: string,
|
||||
@Query('take') take?: number
|
||||
): Promise<Activities> {
|
||||
let endDate: Date;
|
||||
let startDate: Date;
|
||||
|
||||
if (dateRange) {
|
||||
({ endDate, startDate } = getInterval(dateRange));
|
||||
}
|
||||
|
||||
const filters = this.apiService.buildFiltersFromQueryParams({
|
||||
filterByAccounts,
|
||||
filterByAssetClasses,
|
||||
filterByTags
|
||||
});
|
||||
|
||||
const { endDate, startDate } = getInterval(dateRange);
|
||||
|
||||
const impersonationUserId =
|
||||
await this.impersonationService.validateImpersonationId(impersonationId);
|
||||
const userCurrency = this.request.user.Settings.settings.baseCurrency;
|
||||
|
@ -32,6 +32,7 @@ export class PortfolioCalculatorFactory {
|
||||
calculationType,
|
||||
currency,
|
||||
dateRange = 'max',
|
||||
hasFilters,
|
||||
isExperimentalFeatures = false,
|
||||
userId
|
||||
}: {
|
||||
@ -40,9 +41,12 @@ export class PortfolioCalculatorFactory {
|
||||
calculationType: PerformanceCalculationType;
|
||||
currency: string;
|
||||
dateRange?: DateRange;
|
||||
hasFilters: boolean;
|
||||
isExperimentalFeatures?: boolean;
|
||||
userId: string;
|
||||
}): PortfolioCalculator {
|
||||
const useCache = !hasFilters && isExperimentalFeatures;
|
||||
|
||||
switch (calculationType) {
|
||||
case PerformanceCalculationType.MWR:
|
||||
return new MWRPortfolioCalculator({
|
||||
@ -50,7 +54,7 @@ export class PortfolioCalculatorFactory {
|
||||
activities,
|
||||
currency,
|
||||
dateRange,
|
||||
isExperimentalFeatures,
|
||||
useCache,
|
||||
userId,
|
||||
configurationService: this.configurationService,
|
||||
currentRateService: this.currentRateService,
|
||||
@ -64,7 +68,7 @@ export class PortfolioCalculatorFactory {
|
||||
currency,
|
||||
currentRateService: this.currentRateService,
|
||||
dateRange,
|
||||
isExperimentalFeatures,
|
||||
useCache,
|
||||
userId,
|
||||
configurationService: this.configurationService,
|
||||
exchangeRateDataService: this.exchangeRateDataService,
|
||||
|
@ -37,6 +37,7 @@ import {
|
||||
eachDayOfInterval,
|
||||
endOfDay,
|
||||
format,
|
||||
isAfter,
|
||||
isBefore,
|
||||
isSameDay,
|
||||
max,
|
||||
@ -44,26 +45,26 @@ import {
|
||||
subDays
|
||||
} from 'date-fns';
|
||||
import { first, last, uniq, uniqBy } from 'lodash';
|
||||
import ms from 'ms';
|
||||
|
||||
export abstract class PortfolioCalculator {
|
||||
protected static readonly ENABLE_LOGGING = false;
|
||||
|
||||
protected accountBalanceItems: HistoricalDataItem[];
|
||||
protected orders: PortfolioOrder[];
|
||||
protected activities: PortfolioOrder[];
|
||||
|
||||
private configurationService: ConfigurationService;
|
||||
private currency: string;
|
||||
private currentRateService: CurrentRateService;
|
||||
private dataProviderInfos: DataProviderInfo[];
|
||||
private dateRange: DateRange;
|
||||
private endDate: Date;
|
||||
private exchangeRateDataService: ExchangeRateDataService;
|
||||
private isExperimentalFeatures: boolean;
|
||||
private redisCacheService: RedisCacheService;
|
||||
private snapshot: PortfolioSnapshot;
|
||||
private snapshotPromise: Promise<void>;
|
||||
private startDate: Date;
|
||||
private transactionPoints: TransactionPoint[];
|
||||
private useCache: boolean;
|
||||
private userId: string;
|
||||
|
||||
public constructor({
|
||||
@ -74,8 +75,8 @@ export abstract class PortfolioCalculator {
|
||||
currentRateService,
|
||||
dateRange,
|
||||
exchangeRateDataService,
|
||||
isExperimentalFeatures,
|
||||
redisCacheService,
|
||||
useCache,
|
||||
userId
|
||||
}: {
|
||||
accountBalanceItems: HistoricalDataItem[];
|
||||
@ -85,18 +86,18 @@ export abstract class PortfolioCalculator {
|
||||
currentRateService: CurrentRateService;
|
||||
dateRange: DateRange;
|
||||
exchangeRateDataService: ExchangeRateDataService;
|
||||
isExperimentalFeatures: boolean;
|
||||
redisCacheService: RedisCacheService;
|
||||
useCache: boolean;
|
||||
userId: string;
|
||||
}) {
|
||||
this.accountBalanceItems = accountBalanceItems;
|
||||
this.configurationService = configurationService;
|
||||
this.currency = currency;
|
||||
this.currentRateService = currentRateService;
|
||||
this.dateRange = dateRange;
|
||||
this.exchangeRateDataService = exchangeRateDataService;
|
||||
this.isExperimentalFeatures = isExperimentalFeatures;
|
||||
|
||||
this.orders = activities
|
||||
this.activities = activities
|
||||
.map(
|
||||
({
|
||||
date,
|
||||
@ -107,6 +108,12 @@ export abstract class PortfolioCalculator {
|
||||
type,
|
||||
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 {
|
||||
SymbolProfile,
|
||||
tags,
|
||||
@ -123,6 +130,7 @@ export abstract class PortfolioCalculator {
|
||||
});
|
||||
|
||||
this.redisCacheService = redisCacheService;
|
||||
this.useCache = useCache;
|
||||
this.userId = userId;
|
||||
|
||||
const { endDate, startDate } = getInterval(dateRange);
|
||||
@ -917,7 +925,7 @@ export abstract class PortfolioCalculator {
|
||||
tags,
|
||||
type,
|
||||
unitPrice
|
||||
} of this.orders) {
|
||||
} of this.activities) {
|
||||
let currentTransactionPointItem: TransactionPointSymbol;
|
||||
const oldAccumulatedSymbol = symbols[SymbolProfile.symbol];
|
||||
|
||||
@ -1041,11 +1049,13 @@ export abstract class PortfolioCalculator {
|
||||
}
|
||||
|
||||
private async initialize() {
|
||||
if (this.isExperimentalFeatures) {
|
||||
if (this.useCache) {
|
||||
const startTimeTotal = performance.now();
|
||||
|
||||
const cachedSnapshot = await this.redisCacheService.get(
|
||||
this.redisCacheService.getPortfolioSnapshotKey(this.userId)
|
||||
this.redisCacheService.getPortfolioSnapshotKey({
|
||||
userId: this.userId
|
||||
})
|
||||
);
|
||||
|
||||
if (cachedSnapshot) {
|
||||
@ -1068,7 +1078,9 @@ export abstract class PortfolioCalculator {
|
||||
);
|
||||
|
||||
this.redisCacheService.set(
|
||||
this.redisCacheService.getPortfolioSnapshotKey(this.userId),
|
||||
this.redisCacheService.getPortfolioSnapshotKey({
|
||||
userId: this.userId
|
||||
}),
|
||||
JSON.stringify(this.snapshot),
|
||||
this.configurationService.get('CACHE_QUOTES_TTL')
|
||||
);
|
||||
|
@ -123,6 +123,7 @@ describe('PortfolioCalculator', () => {
|
||||
activities,
|
||||
calculationType: PerformanceCalculationType.TWR,
|
||||
currency: 'CHF',
|
||||
hasFilters: false,
|
||||
userId: userDummyData.id
|
||||
});
|
||||
|
||||
|
@ -108,6 +108,7 @@ describe('PortfolioCalculator', () => {
|
||||
activities,
|
||||
calculationType: PerformanceCalculationType.TWR,
|
||||
currency: 'CHF',
|
||||
hasFilters: false,
|
||||
userId: userDummyData.id
|
||||
});
|
||||
|
||||
|
@ -93,6 +93,7 @@ describe('PortfolioCalculator', () => {
|
||||
activities,
|
||||
calculationType: PerformanceCalculationType.TWR,
|
||||
currency: 'CHF',
|
||||
hasFilters: false,
|
||||
userId: userDummyData.id
|
||||
});
|
||||
|
||||
|
@ -121,6 +121,7 @@ describe('PortfolioCalculator', () => {
|
||||
activities,
|
||||
calculationType: PerformanceCalculationType.TWR,
|
||||
currency: 'CHF',
|
||||
hasFilters: false,
|
||||
userId: userDummyData.id
|
||||
});
|
||||
|
||||
|
@ -93,6 +93,7 @@ describe('PortfolioCalculator', () => {
|
||||
activities,
|
||||
calculationType: PerformanceCalculationType.TWR,
|
||||
currency: 'USD',
|
||||
hasFilters: false,
|
||||
userId: userDummyData.id
|
||||
});
|
||||
|
||||
|
@ -106,6 +106,7 @@ describe('PortfolioCalculator', () => {
|
||||
activities,
|
||||
calculationType: PerformanceCalculationType.TWR,
|
||||
currency: 'CHF',
|
||||
hasFilters: false,
|
||||
userId: userDummyData.id
|
||||
});
|
||||
|
||||
|
@ -93,6 +93,7 @@ describe('PortfolioCalculator', () => {
|
||||
activities,
|
||||
calculationType: PerformanceCalculationType.TWR,
|
||||
currency: 'USD',
|
||||
hasFilters: false,
|
||||
userId: userDummyData.id
|
||||
});
|
||||
|
||||
|
@ -74,7 +74,7 @@ describe('PortfolioCalculator', () => {
|
||||
const activities: Activity[] = [
|
||||
{
|
||||
...activityDummyData,
|
||||
date: new Date('2022-01-01'),
|
||||
date: new Date('2023-01-01'), // Date in future
|
||||
fee: 0,
|
||||
quantity: 1,
|
||||
SymbolProfile: {
|
||||
@ -93,64 +93,16 @@ describe('PortfolioCalculator', () => {
|
||||
activities,
|
||||
calculationType: PerformanceCalculationType.TWR,
|
||||
currency: 'USD',
|
||||
hasFilters: false,
|
||||
userId: userDummyData.id
|
||||
});
|
||||
|
||||
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(
|
||||
parseDate('2022-01-01')
|
||||
);
|
||||
|
||||
spy.mockRestore();
|
||||
|
||||
expect(portfolioSnapshot).toEqual({
|
||||
currentValueInBaseCurrency: new Big('0'),
|
||||
errors: [],
|
||||
grossPerformance: new Big('0'),
|
||||
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')
|
||||
});
|
||||
const liabilitiesInBaseCurrency =
|
||||
await portfolioCalculator.getLiabilitiesInBaseCurrency();
|
||||
|
||||
expect(liabilitiesInBaseCurrency).toEqual(new Big(3000));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -121,6 +121,7 @@ describe('PortfolioCalculator', () => {
|
||||
activities,
|
||||
calculationType: PerformanceCalculationType.TWR,
|
||||
currency: 'USD',
|
||||
hasFilters: false,
|
||||
userId: userDummyData.id
|
||||
});
|
||||
|
||||
|
@ -71,6 +71,7 @@ describe('PortfolioCalculator', () => {
|
||||
activities: [],
|
||||
calculationType: PerformanceCalculationType.TWR,
|
||||
currency: 'CHF',
|
||||
hasFilters: false,
|
||||
userId: userDummyData.id
|
||||
});
|
||||
|
||||
|
@ -108,6 +108,7 @@ describe('PortfolioCalculator', () => {
|
||||
activities,
|
||||
calculationType: PerformanceCalculationType.TWR,
|
||||
currency: 'CHF',
|
||||
hasFilters: false,
|
||||
userId: userDummyData.id
|
||||
});
|
||||
|
||||
|
@ -108,6 +108,7 @@ describe('PortfolioCalculator', () => {
|
||||
activities,
|
||||
calculationType: PerformanceCalculationType.TWR,
|
||||
currency: 'CHF',
|
||||
hasFilters: false,
|
||||
userId: userDummyData.id
|
||||
});
|
||||
|
||||
|
@ -203,7 +203,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
||||
let valueAtStartDateWithCurrencyEffect: Big;
|
||||
|
||||
// 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 }) => {
|
||||
return SymbolProfile.symbol === symbol;
|
||||
}
|
||||
|
@ -8,6 +8,13 @@ import { GetValuesParams } from './interfaces/get-values-params.interface';
|
||||
|
||||
function mockGetValue(symbol: string, date: Date) {
|
||||
switch (symbol) {
|
||||
case '55196015-1365-4560-aa60-8751ae6d18f8':
|
||||
if (isSameDay(parseDate('2022-01-31'), date)) {
|
||||
return { marketPrice: 3000 };
|
||||
}
|
||||
|
||||
return { marketPrice: 0 };
|
||||
|
||||
case 'BALN.SW':
|
||||
if (isSameDay(parseDate('2021-11-12'), date)) {
|
||||
return { marketPrice: 146 };
|
||||
|
@ -277,9 +277,11 @@ export class PortfolioService {
|
||||
|
||||
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
||||
activities,
|
||||
dateRange,
|
||||
userId,
|
||||
calculationType: PerformanceCalculationType.TWR,
|
||||
currency: this.request.user.Settings.settings.baseCurrency,
|
||||
hasFilters: filters?.length > 0,
|
||||
isExperimentalFeatures:
|
||||
this.request.user.Settings.settings.isExperimentalFeatures
|
||||
});
|
||||
@ -358,8 +360,9 @@ export class PortfolioService {
|
||||
userId,
|
||||
calculationType: PerformanceCalculationType.TWR,
|
||||
currency: userCurrency,
|
||||
hasFilters: filters?.length > 0,
|
||||
isExperimentalFeatures:
|
||||
this.request.user.Settings.settings.isExperimentalFeatures
|
||||
this.request.user?.Settings.settings.isExperimentalFeatures
|
||||
});
|
||||
|
||||
const { currentValueInBaseCurrency, hasErrors, positions } =
|
||||
@ -660,6 +663,7 @@ export class PortfolioService {
|
||||
}),
|
||||
calculationType: PerformanceCalculationType.TWR,
|
||||
currency: userCurrency,
|
||||
hasFilters: true,
|
||||
isExperimentalFeatures:
|
||||
this.request.user.Settings.settings.isExperimentalFeatures
|
||||
});
|
||||
@ -700,17 +704,19 @@ export class PortfolioService {
|
||||
|
||||
const dividendYieldPercent = this.getAnnualizedPerformancePercent({
|
||||
daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)),
|
||||
netPerformancePercent: dividendInBaseCurrency.div(
|
||||
timeWeightedInvestment
|
||||
)
|
||||
netPerformancePercent: timeWeightedInvestment.eq(0)
|
||||
? new Big(0)
|
||||
: dividendInBaseCurrency.div(timeWeightedInvestment)
|
||||
});
|
||||
|
||||
const dividendYieldPercentWithCurrencyEffect =
|
||||
this.getAnnualizedPerformancePercent({
|
||||
daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)),
|
||||
netPerformancePercent: dividendInBaseCurrency.div(
|
||||
timeWeightedInvestmentWithCurrencyEffect
|
||||
)
|
||||
netPerformancePercent: timeWeightedInvestmentWithCurrencyEffect.eq(0)
|
||||
? new Big(0)
|
||||
: dividendInBaseCurrency.div(
|
||||
timeWeightedInvestmentWithCurrencyEffect
|
||||
)
|
||||
});
|
||||
|
||||
const historicalData = await this.dataProviderService.getHistorical(
|
||||
@ -931,6 +937,7 @@ export class PortfolioService {
|
||||
userId,
|
||||
calculationType: PerformanceCalculationType.TWR,
|
||||
currency: this.request.user.Settings.settings.baseCurrency,
|
||||
hasFilters: filters?.length > 0,
|
||||
isExperimentalFeatures:
|
||||
this.request.user.Settings.settings.isExperimentalFeatures
|
||||
});
|
||||
@ -1085,7 +1092,7 @@ export class PortfolioService {
|
||||
)
|
||||
);
|
||||
|
||||
const { endDate, startDate } = getInterval(dateRange);
|
||||
const { endDate } = getInterval(dateRange);
|
||||
|
||||
const { activities } = await this.orderService.getOrders({
|
||||
endDate,
|
||||
@ -1123,6 +1130,7 @@ export class PortfolioService {
|
||||
userId,
|
||||
calculationType: PerformanceCalculationType.TWR,
|
||||
currency: userCurrency,
|
||||
hasFilters: filters?.length > 0,
|
||||
isExperimentalFeatures:
|
||||
this.request.user.Settings.settings.isExperimentalFeatures
|
||||
});
|
||||
@ -1220,6 +1228,7 @@ export class PortfolioService {
|
||||
userId,
|
||||
calculationType: PerformanceCalculationType.TWR,
|
||||
currency: this.request.user.Settings.settings.baseCurrency,
|
||||
hasFilters: false,
|
||||
isExperimentalFeatures:
|
||||
this.request.user.Settings.settings.isExperimentalFeatures
|
||||
});
|
||||
|
@ -24,7 +24,7 @@ export class RedisCacheService {
|
||||
return this.cache.get(key);
|
||||
}
|
||||
|
||||
public getPortfolioSnapshotKey(userId: string) {
|
||||
public getPortfolioSnapshotKey({ userId }: { userId: string }) {
|
||||
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 { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||
import { User, UserSettings } from '@ghostfolio/common/interfaces';
|
||||
import {
|
||||
hasPermission,
|
||||
hasRole,
|
||||
permissions
|
||||
} from '@ghostfolio/common/permissions';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
|
||||
import {
|
||||
@ -63,13 +59,6 @@ export class UserController {
|
||||
public async getUser(
|
||||
@Headers('accept-language') acceptLanguage: string
|
||||
): Promise<User> {
|
||||
if (hasRole(this.request.user, 'INACTIVE')) {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
|
||||
StatusCodes.TOO_MANY_REQUESTS
|
||||
);
|
||||
}
|
||||
|
||||
return this.userService.getUser(
|
||||
this.request.user,
|
||||
acceptLanguage?.split(',')?.[0]
|
||||
|
@ -17,7 +17,9 @@ export class PortfolioChangedListener {
|
||||
);
|
||||
|
||||
this.redisCacheService.remove(
|
||||
this.redisCacheService.getPortfolioSnapshotKey(event.getUserId())
|
||||
this.redisCacheService.getPortfolioSnapshotKey({
|
||||
userId: event.getUserId()
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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 { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { validateObjectForForm } from '@ghostfolio/client/util/form.util';
|
||||
import { ghostfolioScraperApiSymbolPrefix } from '@ghostfolio/common/config';
|
||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
@ -258,7 +259,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
public onSubmit() {
|
||||
public async onSubmit() {
|
||||
let countries = [];
|
||||
let scraperConfiguration = {};
|
||||
let sectors = [];
|
||||
@ -299,6 +300,17 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
||||
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
|
||||
.patchAssetProfile({
|
||||
...assetProfileData,
|
||||
|
@ -143,9 +143,7 @@ export class AdminPlatformComponent implements OnInit, OnDestroy {
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((data) => {
|
||||
const platform: CreatePlatformDto = data?.platform;
|
||||
|
||||
.subscribe((platform: CreatePlatformDto | null) => {
|
||||
if (platform) {
|
||||
this.adminService
|
||||
.postPlatform(platform)
|
||||
@ -182,9 +180,7 @@ export class AdminPlatformComponent implements OnInit, OnDestroy {
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((data) => {
|
||||
const platform: UpdatePlatformDto = data?.platform;
|
||||
|
||||
.subscribe((platform: UpdatePlatformDto | null) => {
|
||||
if (platform) {
|
||||
this.adminService
|
||||
.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 { Subject } from 'rxjs';
|
||||
|
||||
@ -11,18 +21,54 @@ import { CreateOrUpdatePlatformDialogParams } from './interfaces/interfaces';
|
||||
styleUrls: ['./create-or-update-platform-dialog.scss'],
|
||||
templateUrl: 'create-or-update-platform-dialog.html'
|
||||
})
|
||||
export class CreateOrUpdatePlatformDialog {
|
||||
export class CreateOrUpdatePlatformDialog implements OnDestroy {
|
||||
public platformForm: FormGroup;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor(
|
||||
@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() {
|
||||
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() {
|
||||
this.unsubscribeSubject.next();
|
||||
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>Add platform</h1>
|
||||
<div class="flex-grow-1 py-3" mat-dialog-content>
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<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>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<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) {
|
||||
<gf-asset-profile-icon
|
||||
class="mr-3"
|
||||
@ -23,12 +36,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
[disabled]="!addPlatformForm.form.valid"
|
||||
[mat-dialog-close]="data"
|
||||
type="submit"
|
||||
[disabled]="!platformForm.valid"
|
||||
>
|
||||
<ng-container i18n>Save</ng-container>
|
||||
</button>
|
||||
|
@ -142,9 +142,7 @@ export class AdminTagComponent implements OnInit, OnDestroy {
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((data) => {
|
||||
const tag: CreateTagDto = data?.tag;
|
||||
|
||||
.subscribe((tag: CreateTagDto | null) => {
|
||||
if (tag) {
|
||||
this.adminService
|
||||
.postTag(tag)
|
||||
@ -180,9 +178,7 @@ export class AdminTagComponent implements OnInit, OnDestroy {
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((data) => {
|
||||
const tag: UpdateTagDto = data?.tag;
|
||||
|
||||
.subscribe((tag: UpdateTagDto | null) => {
|
||||
if (tag) {
|
||||
this.adminService
|
||||
.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 { Subject } from 'rxjs';
|
||||
|
||||
@ -11,18 +21,52 @@ import { CreateOrUpdateTagDialogParams } from './interfaces/interfaces';
|
||||
styleUrls: ['./create-or-update-tag-dialog.scss'],
|
||||
templateUrl: 'create-or-update-tag-dialog.html'
|
||||
})
|
||||
export class CreateOrUpdateTagDialog {
|
||||
export class CreateOrUpdateTagDialog implements OnDestroy {
|
||||
public tagForm: FormGroup;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor(
|
||||
@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() {
|
||||
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() {
|
||||
this.unsubscribeSubject.next();
|
||||
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>Add tag</h1>
|
||||
<div class="flex-grow-1 py-3" mat-dialog-content>
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
[disabled]="!addTagForm.form.valid"
|
||||
[mat-dialog-close]="data"
|
||||
type="submit"
|
||||
[disabled]="!tagForm.valid"
|
||||
>
|
||||
<ng-container i18n>Save</ng-container>
|
||||
</button>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { validateObjectForForm } from '@ghostfolio/client/util/form.util';
|
||||
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
@ -40,22 +41,22 @@ export class CreateOrUpdateAccessDialog implements OnDestroy {
|
||||
alias: [this.data.access.alias],
|
||||
permissions: [this.data.access.permissions[0], 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) => {
|
||||
const granteeUserIdControl = this.accessForm.get('granteeUserId');
|
||||
const permissionsControl = this.accessForm.get('permissions');
|
||||
const userIdControl = this.accessForm.get('userId');
|
||||
|
||||
if (accessType === 'PRIVATE') {
|
||||
granteeUserIdControl.setValidators(Validators.required);
|
||||
permissionsControl.setValidators(Validators.required);
|
||||
userIdControl.setValidators(Validators.required);
|
||||
} else {
|
||||
userIdControl.clearValidators();
|
||||
granteeUserIdControl.clearValidators();
|
||||
}
|
||||
|
||||
granteeUserIdControl.updateValueAndValidity();
|
||||
permissionsControl.updateValueAndValidity();
|
||||
userIdControl.updateValueAndValidity();
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
@ -65,28 +66,38 @@ export class CreateOrUpdateAccessDialog implements OnDestroy {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
public onSubmit() {
|
||||
public async onSubmit() {
|
||||
const access: CreateAccessDto = {
|
||||
alias: this.accessForm.get('alias').value,
|
||||
granteeUserId: this.accessForm.get('userId').value,
|
||||
granteeUserId: this.accessForm.get('granteeUserId').value,
|
||||
permissions: [this.accessForm.get('permissions').value]
|
||||
};
|
||||
|
||||
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 });
|
||||
try {
|
||||
await validateObjectForForm({
|
||||
classDto: CreateAccessDto,
|
||||
form: this.accessForm,
|
||||
object: 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() {
|
||||
|
@ -45,7 +45,7 @@
|
||||
Ghostfolio <ng-container i18n>User ID</ng-container>
|
||||
</mat-label>
|
||||
<input
|
||||
formControlName="userId"
|
||||
formControlName="granteeUserId"
|
||||
matInput
|
||||
type="text"
|
||||
(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 { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { Access, User } from '@ghostfolio/common/interfaces';
|
||||
@ -113,7 +114,7 @@ export class UserAccountAccessComponent implements OnDestroy, OnInit {
|
||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((access) => {
|
||||
dialogRef.afterClosed().subscribe((access: CreateAccessDto | null) => {
|
||||
if (access) {
|
||||
this.update();
|
||||
}
|
||||
|
@ -54,9 +54,10 @@ export class AuthGuard {
|
||||
this.router.navigate(['/' + $localize`register`]);
|
||||
resolve(false);
|
||||
} else if (
|
||||
AuthGuard.PUBLIC_PAGE_ROUTES.filter((publicPageRoute) =>
|
||||
state.url.startsWith(publicPageRoute)
|
||||
)?.length > 0
|
||||
AuthGuard.PUBLIC_PAGE_ROUTES.filter((publicPageRoute) => {
|
||||
const [, url] = state.url.split('/');
|
||||
return `/${url}` === publicPageRoute;
|
||||
})?.length > 0
|
||||
) {
|
||||
resolve(true);
|
||||
return EMPTY;
|
||||
|
@ -189,9 +189,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((data: any) => {
|
||||
const account: UpdateAccountDto = data?.account;
|
||||
|
||||
.subscribe((account: UpdateAccountDto | null) => {
|
||||
if (account) {
|
||||
this.dataService
|
||||
.putAccount(account)
|
||||
@ -258,9 +256,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((data: any) => {
|
||||
const account: CreateAccountDto = data?.account;
|
||||
|
||||
.subscribe((account: CreateAccountDto | null) => {
|
||||
if (account) {
|
||||
this.dataService
|
||||
.postAccount(account)
|
||||
|
@ -123,6 +123,8 @@ export class CreateOrUpdateAccountDialog implements OnDestroy {
|
||||
form: this.accountForm,
|
||||
object: account
|
||||
});
|
||||
|
||||
this.dialogRef.close(account as UpdateAccountDto);
|
||||
} else {
|
||||
delete (account as CreateAccountDto).id;
|
||||
|
||||
@ -131,9 +133,9 @@ export class CreateOrUpdateAccountDialog implements OnDestroy {
|
||||
form: this.accountForm,
|
||||
object: account
|
||||
});
|
||||
}
|
||||
|
||||
this.dialogRef.close({ account });
|
||||
this.dialogRef.close(account as CreateAccountDto);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
@ -287,9 +287,7 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((data: any) => {
|
||||
const transaction: UpdateOrderDto = data?.activity;
|
||||
|
||||
.subscribe((transaction: UpdateOrderDto | null) => {
|
||||
if (transaction) {
|
||||
this.dataService
|
||||
.putOrder(transaction)
|
||||
@ -338,9 +336,7 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((data: any) => {
|
||||
const transaction: CreateOrderDto = data?.activity;
|
||||
|
||||
.subscribe((transaction: CreateOrderDto | null) => {
|
||||
if (transaction) {
|
||||
this.dataService.postOrder(transaction).subscribe({
|
||||
next: () => {
|
||||
|
@ -475,6 +475,8 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
||||
ignoreFields: ['dataSource', 'date'],
|
||||
object: activity as UpdateOrderDto
|
||||
});
|
||||
|
||||
this.dialogRef.close(activity as UpdateOrderDto);
|
||||
} else {
|
||||
(activity as CreateOrderDto).updateAccountBalance =
|
||||
this.activityForm.get('updateAccountBalance').value;
|
||||
@ -485,9 +487,9 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
||||
ignoreFields: ['dataSource', 'date'],
|
||||
object: activity
|
||||
});
|
||||
}
|
||||
|
||||
this.dialogRef.close({ activity });
|
||||
this.dialogRef.close(activity as CreateOrderDto);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
@ -32,6 +32,14 @@ export async function validateObjectForForm<T>({
|
||||
validationError: Object.values(constraints)[0]
|
||||
});
|
||||
}
|
||||
|
||||
const formControlInCustomCurrency = form.get(`${property}InCustomCurrency`);
|
||||
|
||||
if (formControlInCustomCurrency) {
|
||||
formControlInCustomCurrency.setErrors({
|
||||
validationError: Object.values(constraints)[0]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(nonIgnoredErrors);
|
||||
|
@ -109,7 +109,7 @@
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="performance">
|
||||
<ng-container matColumnDef="performance" stickyEnd>
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="justify-content-end px-1"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ghostfolio",
|
||||
"version": "2.77.0",
|
||||
"version": "2.78.0",
|
||||
"homepage": "https://ghostfol.io",
|
||||
"license": "AGPL-3.0",
|
||||
"repository": "https://github.com/ghostfolio/ghostfolio",
|
||||
|
Reference in New Issue
Block a user