Compare commits
49 Commits
Author | SHA1 | Date | |
---|---|---|---|
54c5746d21 | |||
7130ac7565 | |||
1851ae137f | |||
6f6ff94979 | |||
7f25066f0f | |||
fc795aaa8c | |||
d0112968e8 | |||
522025ffa0 | |||
27bf662281 | |||
93c27277c6 | |||
5e6adfcef5 | |||
ab691bb27a | |||
8fc5676443 | |||
1fe1e2fe0c | |||
921d38a706 | |||
6161d5e77c | |||
369386f976 | |||
41437636b1 | |||
b21884eb66 | |||
1c5437e1fd | |||
58278ba5e6 | |||
921f3e9807 | |||
75ca125a70 | |||
a1fd4e7a38 | |||
0d5a8eb33e | |||
b088df2fa3 | |||
f45d8f616a | |||
d8300502ce | |||
502d51ad29 | |||
bc33e5f147 | |||
48ba8f936b | |||
05ec4cce05 | |||
d74f283707 | |||
0f8bc7db32 | |||
431500f28a | |||
9672de174e | |||
c6aa06b933 | |||
1f46a6b6f3 | |||
1bed940bc0 | |||
f9eb3cc3c5 | |||
2519c3ffb0 | |||
91013d1d10 | |||
6deefb9c43 | |||
d0744e07df | |||
93e1ee3ba7 | |||
dceaa55a6c | |||
8b4d55925d | |||
754b49e50f | |||
6ccbda8169 |
@ -11,6 +11,5 @@ POSTGRES_USER=user
|
||||
POSTGRES_PASSWORD=<INSERT_POSTGRES_PASSWORD>
|
||||
|
||||
ACCESS_TOKEN_SALT=<INSERT_RANDOM_STRING>
|
||||
ALPHA_VANTAGE_API_KEY=
|
||||
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?connect_timeout=300&sslmode=prefer
|
||||
JWT_SECRET_KEY=<INSERT_RANDOM_STRING>
|
||||
|
88
CHANGELOG.md
88
CHANGELOG.md
@ -5,6 +5,90 @@ 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.3.0 - 2023-09-17
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for fees on account level (experimental)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the export functionality for liabilities
|
||||
|
||||
## 2.2.0 - 2023-09-17
|
||||
|
||||
### Added
|
||||
|
||||
- Introduced a sidebar navigation on desktop
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the style of the system message
|
||||
- Upgraded _Postgres_ from version `12` to `15` in the `docker-compose` files
|
||||
|
||||
## 2.1.0 - 2023-09-15
|
||||
|
||||
### Added
|
||||
|
||||
- Added support to drop a file in the import activities dialog
|
||||
- Added a timeout to all data source requests
|
||||
|
||||
### Changed
|
||||
|
||||
- Harmonized the style of the user interface for granting and revoking public access to share the portfolio
|
||||
- Removed the account type from the user interface as a preparation to remove it from the `Account` database schema
|
||||
- Improved the logger output of the info service
|
||||
- Harmonized the logger output: `<symbol> (<dataSource>)`
|
||||
- Improved the language localization for German (`de`)
|
||||
- Improved the language localization for Italian (`it`)
|
||||
- Improved the language localization for Dutch (`nl`)
|
||||
- Improved the read-only mode
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the timeout in _EOD Historical Data_ requests
|
||||
- Fixed an issue with the portfolio summary caused by the language localization for Dutch (`nl`)
|
||||
|
||||
## 2.0.0 - 2023-09-09
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for the cryptocurrency _CyberConnect_
|
||||
- Added a blog post: _Announcing Ghostfolio 2.0_
|
||||
|
||||
### Changed
|
||||
|
||||
- **Breaking Change**: Removed the deprecated environment variable `BASE_CURRENCY`
|
||||
- Improved the validation in the activities import
|
||||
- Deactivated _Internet Identity_ as a social login provider for the account registration
|
||||
- Improved the language localization for German (`de`)
|
||||
- Refreshed the cryptocurrencies list
|
||||
- Changed the version in the `docker-compose` files from `3.7` to `3.9`
|
||||
- Upgraded `yahoo-finance2` from version `2.4.4` to `2.5.0`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue in the _Yahoo Finance_ data enhancer where countries and sectors have been removed
|
||||
|
||||
## 1.305.0 - 2023-09-03
|
||||
|
||||
### Added
|
||||
|
||||
- Added _Hacker News_ to the _As seen in_ section on the landing page
|
||||
|
||||
### Changed
|
||||
|
||||
- Shortened the page titles
|
||||
- Improved the language localization for German (`de`)
|
||||
- Upgraded `prisma` from version `4.16.2` to `5.2.0`
|
||||
- Upgraded `replace-in-file` from version `6.3.5` to `7.0.1`
|
||||
- Upgraded `yahoo-finance2` from version `2.4.3` to `2.4.4`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the alignment in the header navigation
|
||||
- Fixed the alignment in the menu of the impersonation mode
|
||||
|
||||
## 1.304.0 - 2023-08-27
|
||||
|
||||
### Added
|
||||
@ -1469,7 +1553,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- Set up the language localization for Italiano (`it`)
|
||||
- Set up the language localization for Italian (`it`)
|
||||
- Extended the landing page
|
||||
|
||||
## 1.195.0 - 20.09.2022
|
||||
@ -2892,7 +2976,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
- Supported the management of additional currencies in the admin control panel
|
||||
- Introduced the system message
|
||||
- Introduced the read only mode
|
||||
- Introduced the read-only mode
|
||||
|
||||
### Changed
|
||||
|
||||
|
@ -13,6 +13,8 @@
|
||||
[](#contributing)
|
||||
[](https://www.gnu.org/licenses/agpl-3.0)
|
||||
|
||||
New: [Ghostfolio 2.0](https://ghostfol.io/en/blog/2023/09/ghostfolio-2)
|
||||
|
||||
</div>
|
||||
|
||||
**Ghostfolio** is an open source wealth management software built with web technology. The application empowers busy people to keep track of stocks, ETFs or cryptocurrencies and make solid, data-driven investment decisions. The software is designed for personal use in continuous operation.
|
||||
@ -136,9 +138,9 @@ docker-compose --env-file ./.env -f docker/docker-compose.build.yml up -d
|
||||
1. Run the following command to start the new Docker image: `docker-compose --env-file ./.env -f docker/docker-compose.yml up -d`
|
||||
At each start, the container will automatically apply the database schema migrations if needed.
|
||||
|
||||
### Run with _Unraid_ (Community)
|
||||
### Home Server Systems (Community)
|
||||
|
||||
Please follow the instructions of the Ghostfolio [Unraid Community App](https://unraid.net/community/apps?q=ghostfolio).
|
||||
Ghostfolio is available for various home server systems, including [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
|
||||
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
import { isString } from 'lodash';
|
||||
|
||||
export class CreateAccountDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
accountType: AccountType;
|
||||
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
import { isString } from 'lodash';
|
||||
|
||||
export class UpdateAccountDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
accountType: AccountType;
|
||||
|
||||
|
@ -6,7 +6,12 @@ import { MarketDataService } from '@ghostfolio/api/services/market-data/market-d
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
||||
import { PROPERTY_CURRENCIES } from '@ghostfolio/common/config';
|
||||
import {
|
||||
DEFAULT_CURRENCY,
|
||||
PROPERTY_CURRENCIES,
|
||||
PROPERTY_IS_READ_ONLY_MODE,
|
||||
PROPERTY_IS_USER_SIGNUP_ENABLED
|
||||
} from '@ghostfolio/common/config';
|
||||
import {
|
||||
AdminData,
|
||||
AdminMarketData,
|
||||
@ -23,8 +28,6 @@ import { groupBy } from 'lodash';
|
||||
|
||||
@Injectable()
|
||||
export class AdminService {
|
||||
private baseCurrency: string;
|
||||
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService,
|
||||
private readonly dataProviderService: DataProviderService,
|
||||
@ -34,9 +37,7 @@ export class AdminService {
|
||||
private readonly propertyService: PropertyService,
|
||||
private readonly subscriptionService: SubscriptionService,
|
||||
private readonly symbolProfileService: SymbolProfileService
|
||||
) {
|
||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
||||
}
|
||||
) {}
|
||||
|
||||
public async addAssetProfile({
|
||||
dataSource,
|
||||
@ -80,15 +81,15 @@ export class AdminService {
|
||||
exchangeRates: this.exchangeRateDataService
|
||||
.getCurrencies()
|
||||
.filter((currency) => {
|
||||
return currency !== this.baseCurrency;
|
||||
return currency !== DEFAULT_CURRENCY;
|
||||
})
|
||||
.map((currency) => {
|
||||
return {
|
||||
label1: this.baseCurrency,
|
||||
label1: DEFAULT_CURRENCY,
|
||||
label2: currency,
|
||||
value: this.exchangeRateDataService.toCurrency(
|
||||
1,
|
||||
this.baseCurrency,
|
||||
DEFAULT_CURRENCY,
|
||||
currency
|
||||
)
|
||||
};
|
||||
@ -306,7 +307,9 @@ export class AdminService {
|
||||
response = await this.propertyService.delete({ key });
|
||||
}
|
||||
|
||||
if (key === PROPERTY_CURRENCIES) {
|
||||
if (key === PROPERTY_IS_READ_ONLY_MODE && value === 'true') {
|
||||
await this.putSetting(PROPERTY_IS_USER_SIGNUP_ENABLED, 'false');
|
||||
} else if (key === PROPERTY_CURRENCIES) {
|
||||
await this.exchangeRateDataService.initialize();
|
||||
}
|
||||
|
||||
|
@ -55,7 +55,7 @@ export class AuthService {
|
||||
const isUserSignupEnabled =
|
||||
await this.propertyService.isUserSignupEnabled();
|
||||
|
||||
if (!isUserSignupEnabled) {
|
||||
if (!isUserSignupEnabled || true) {
|
||||
throw new Error('Sign up forbidden');
|
||||
}
|
||||
|
||||
|
@ -26,18 +26,8 @@ export class ExportService {
|
||||
where: { userId }
|
||||
})
|
||||
).map(
|
||||
({
|
||||
accountType,
|
||||
balance,
|
||||
comment,
|
||||
currency,
|
||||
id,
|
||||
isExcluded,
|
||||
name,
|
||||
platformId
|
||||
}) => {
|
||||
({ balance, comment, currency, id, isExcluded, name, platformId }) => {
|
||||
return {
|
||||
accountType,
|
||||
balance,
|
||||
comment,
|
||||
currency,
|
||||
@ -87,7 +77,10 @@ export class ExportService {
|
||||
currency: SymbolProfile.currency,
|
||||
dataSource: SymbolProfile.dataSource,
|
||||
date: date.toISOString(),
|
||||
symbol: type === 'ITEM' ? SymbolProfile.name : SymbolProfile.symbol
|
||||
symbol:
|
||||
type === 'FEE' || type === 'ITEM' || type === 'LIABILITY'
|
||||
? SymbolProfile.name
|
||||
: SymbolProfile.symbol
|
||||
};
|
||||
}
|
||||
)
|
||||
|
@ -566,7 +566,7 @@ export class ImportService {
|
||||
])
|
||||
)?.[symbol];
|
||||
|
||||
if (!assetProfile) {
|
||||
if (!assetProfile?.name) {
|
||||
throw new Error(
|
||||
`activities.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")`
|
||||
);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { BenchmarkModule } from '@ghostfolio/api/app/benchmark/benchmark.module';
|
||||
import { PlatformModule } from '@ghostfolio/api/app/platform/platform.module';
|
||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||
@ -28,11 +29,11 @@ import { InfoService } from './info.service';
|
||||
signOptions: { expiresIn: '30 days' }
|
||||
}),
|
||||
PlatformModule,
|
||||
PrismaModule,
|
||||
PropertyModule,
|
||||
RedisCacheModule,
|
||||
SymbolProfileModule,
|
||||
TagModule
|
||||
TagModule,
|
||||
UserModule
|
||||
],
|
||||
providers: [InfoService]
|
||||
})
|
||||
|
@ -1,12 +1,14 @@
|
||||
import { BenchmarkService } from '@ghostfolio/api/app/benchmark/benchmark.service';
|
||||
import { PlatformService } from '@ghostfolio/api/app/platform/platform.service';
|
||||
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
||||
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||
import { TagService } from '@ghostfolio/api/services/tag/tag.service';
|
||||
import {
|
||||
DEFAULT_CURRENCY,
|
||||
DEFAULT_REQUEST_TIMEOUT,
|
||||
PROPERTY_BETTER_UPTIME_MONITOR_ID,
|
||||
PROPERTY_COUNTRIES_OF_SUBSCRIBERS,
|
||||
PROPERTY_DEMO_USER_ID,
|
||||
@ -44,10 +46,10 @@ export class InfoService {
|
||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly platformService: PlatformService,
|
||||
private readonly prismaService: PrismaService,
|
||||
private readonly propertyService: PropertyService,
|
||||
private readonly redisCacheService: RedisCacheService,
|
||||
private readonly tagService: TagService
|
||||
private readonly tagService: TagService,
|
||||
private readonly userService: UserService
|
||||
) {}
|
||||
|
||||
public async get(): Promise<InfoItem> {
|
||||
@ -139,18 +141,13 @@ export class InfoService {
|
||||
subscriptions,
|
||||
systemMessage,
|
||||
tags,
|
||||
baseCurrency: this.configurationService.get('BASE_CURRENCY'),
|
||||
baseCurrency: DEFAULT_CURRENCY,
|
||||
currencies: this.exchangeRateDataService.getCurrencies()
|
||||
};
|
||||
}
|
||||
|
||||
private async countActiveUsers(aDays: number) {
|
||||
return await this.prismaService.user.count({
|
||||
orderBy: {
|
||||
Analytics: {
|
||||
updatedAt: 'desc'
|
||||
}
|
||||
},
|
||||
return this.userService.count({
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
@ -172,16 +169,24 @@ export class InfoService {
|
||||
|
||||
private async countDockerHubPulls(): Promise<number> {
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const { pull_count } = await got(
|
||||
`https://hub.docker.com/v2/repositories/ghostfolio/ghostfolio`,
|
||||
{
|
||||
headers: { 'User-Agent': 'request' }
|
||||
headers: { 'User-Agent': 'request' },
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
).json<any>();
|
||||
|
||||
return pull_count;
|
||||
} catch (error) {
|
||||
Logger.error(error, 'InfoService');
|
||||
Logger.error(error, 'InfoService - DockerHub');
|
||||
|
||||
return undefined;
|
||||
}
|
||||
@ -189,7 +194,16 @@ export class InfoService {
|
||||
|
||||
private async countGitHubContributors(): Promise<number> {
|
||||
try {
|
||||
const { body } = await got('https://github.com/ghostfolio/ghostfolio');
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const { body } = await got('https://github.com/ghostfolio/ghostfolio', {
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
});
|
||||
|
||||
const $ = cheerio.load(body);
|
||||
|
||||
@ -199,7 +213,7 @@ export class InfoService {
|
||||
).text()
|
||||
);
|
||||
} catch (error) {
|
||||
Logger.error(error, 'InfoService');
|
||||
Logger.error(error, 'InfoService - GitHub');
|
||||
|
||||
return undefined;
|
||||
}
|
||||
@ -207,26 +221,31 @@ export class InfoService {
|
||||
|
||||
private async countGitHubStargazers(): Promise<number> {
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const { stargazers_count } = await got(
|
||||
`https://api.github.com/repos/ghostfolio/ghostfolio`,
|
||||
{
|
||||
headers: { 'User-Agent': 'request' }
|
||||
headers: { 'User-Agent': 'request' },
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
).json<any>();
|
||||
|
||||
return stargazers_count;
|
||||
} catch (error) {
|
||||
Logger.error(error, 'InfoService');
|
||||
Logger.error(error, 'InfoService - GitHub');
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async countNewUsers(aDays: number) {
|
||||
return await this.prismaService.user.count({
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
},
|
||||
return this.userService.count({
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
@ -317,11 +336,10 @@ export class InfoService {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const stripeConfig = (await this.prismaService.property.findUnique({
|
||||
where: { key: PROPERTY_STRIPE_CONFIG }
|
||||
})) ?? { value: '{}' };
|
||||
|
||||
return JSON.parse(stripeConfig.value);
|
||||
return (
|
||||
((await this.propertyService.getByKey(PROPERTY_STRIPE_CONFIG)) as any) ??
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
private async getUptime(): Promise<number> {
|
||||
@ -331,24 +349,31 @@ export class InfoService {
|
||||
PROPERTY_BETTER_UPTIME_MONITOR_ID
|
||||
)) as string;
|
||||
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const { data } = await got(
|
||||
`https://betteruptime.com/api/v2/monitors/${monitorId}/sla?from=${format(
|
||||
`https://uptime.betterstack.com/api/v2/monitors/${monitorId}/sla?from=${format(
|
||||
subDays(new Date(), 90),
|
||||
DATE_FORMAT
|
||||
)}&to${format(new Date(), DATE_FORMAT)}`,
|
||||
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.configurationService.get(
|
||||
'BETTER_UPTIME_API_KEY'
|
||||
)}`
|
||||
}
|
||||
},
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
).json<any>();
|
||||
|
||||
return data.attributes.availability / 100;
|
||||
} catch (error) {
|
||||
Logger.error(error, 'InfoService');
|
||||
Logger.error(error, 'InfoService - Better Stack');
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
||||
import { DEFAULT_REQUEST_TIMEOUT } from '@ghostfolio/common/config';
|
||||
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||
import { HttpException, Injectable } from '@nestjs/common';
|
||||
import { DataSource } from '@prisma/client';
|
||||
@ -41,10 +42,18 @@ export class LogoService {
|
||||
}
|
||||
|
||||
private getBuffer(aUrl: string) {
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
return got(
|
||||
`https://t0.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${aUrl}&size=64`,
|
||||
{
|
||||
headers: { 'User-Agent': 'request' }
|
||||
headers: { 'User-Agent': 'request' },
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
).buffer();
|
||||
}
|
||||
|
@ -97,7 +97,11 @@ export class OrderService {
|
||||
const updateAccountBalance = data.updateAccountBalance ?? false;
|
||||
const userId = data.userId;
|
||||
|
||||
if (data.type === 'ITEM' || data.type === 'LIABILITY') {
|
||||
if (
|
||||
data.type === 'FEE' ||
|
||||
data.type === 'ITEM' ||
|
||||
data.type === 'LIABILITY'
|
||||
) {
|
||||
const assetClass = data.assetClass;
|
||||
const assetSubClass = data.assetSubClass;
|
||||
currency = data.SymbolProfile.connectOrCreate.create.currency;
|
||||
@ -151,7 +155,7 @@ export class OrderService {
|
||||
const orderData: Prisma.OrderCreateInput = data;
|
||||
|
||||
const isDraft =
|
||||
data.type === 'LIABILITY'
|
||||
data.type === 'FEE' || data.type === 'ITEM' || data.type === 'LIABILITY'
|
||||
? false
|
||||
: isAfter(data.date as Date, endOfToday());
|
||||
|
||||
@ -197,7 +201,11 @@ export class OrderService {
|
||||
where
|
||||
});
|
||||
|
||||
if (order.type === 'ITEM' || order.type === 'LIABILITY') {
|
||||
if (
|
||||
order.type === 'FEE' ||
|
||||
order.type === 'ITEM' ||
|
||||
order.type === 'LIABILITY'
|
||||
) {
|
||||
await this.symbolProfileService.deleteById(order.symbolProfileId);
|
||||
}
|
||||
|
||||
@ -368,7 +376,11 @@ export class OrderService {
|
||||
|
||||
let isDraft = false;
|
||||
|
||||
if (data.type === 'ITEM' || data.type === 'LIABILITY') {
|
||||
if (
|
||||
data.type === 'FEE' ||
|
||||
data.type === 'ITEM' ||
|
||||
data.type === 'LIABILITY'
|
||||
) {
|
||||
delete data.SymbolProfile.connect;
|
||||
} else {
|
||||
delete data.SymbolProfile.update;
|
||||
|
@ -105,7 +105,6 @@ describe('CurrentRateService', () => {
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
marketDataService = new MarketDataService(null);
|
||||
|
@ -784,7 +784,7 @@ export class PortfolioCalculator {
|
||||
);
|
||||
} else if (!currentPosition.quantity.eq(0)) {
|
||||
Logger.warn(
|
||||
`Missing historical market data for symbol ${currentPosition.symbol}`,
|
||||
`Missing historical market data for ${currentPosition.symbol} (${currentPosition.dataSource})`,
|
||||
'PortfolioCalculator'
|
||||
);
|
||||
hasErrors = true;
|
||||
|
@ -10,7 +10,10 @@ import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interc
|
||||
import { ApiService } from '@ghostfolio/api/services/api/api.service';
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
|
||||
import {
|
||||
DEFAULT_CURRENCY,
|
||||
HEADER_KEY_IMPERSONATION
|
||||
} from '@ghostfolio/common/config';
|
||||
import {
|
||||
PortfolioDetails,
|
||||
PortfolioDividends,
|
||||
@ -47,8 +50,6 @@ import { PortfolioService } from './portfolio.service';
|
||||
|
||||
@Controller('portfolio')
|
||||
export class PortfolioController {
|
||||
private baseCurrency: string;
|
||||
|
||||
public constructor(
|
||||
private readonly accessService: AccessService,
|
||||
private readonly apiService: ApiService,
|
||||
@ -57,9 +58,7 @@ export class PortfolioController {
|
||||
private readonly portfolioService: PortfolioService,
|
||||
@Inject(REQUEST) private readonly request: RequestWithUser,
|
||||
private readonly userService: UserService
|
||||
) {
|
||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
||||
}
|
||||
) {}
|
||||
|
||||
@Get('details')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@ -442,8 +441,7 @@ export class PortfolioController {
|
||||
return this.exchangeRateDataService.toCurrency(
|
||||
portfolioPosition.quantity * portfolioPosition.marketPrice,
|
||||
portfolioPosition.currency,
|
||||
this.request.user?.Settings?.settings.baseCurrency ??
|
||||
this.baseCurrency
|
||||
this.request.user?.Settings?.settings.baseCurrency ?? DEFAULT_CURRENCY
|
||||
);
|
||||
})
|
||||
.reduce((a, b) => a + b, 0);
|
||||
|
@ -11,12 +11,12 @@ import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/ac
|
||||
import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/base-currency-current-investment';
|
||||
import { CurrencyClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/current-investment';
|
||||
import { FeeRatioInitialInvestment } from '@ghostfolio/api/models/rules/fees/fee-ratio-initial-investment';
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
|
||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
||||
import {
|
||||
DEFAULT_CURRENCY,
|
||||
EMERGENCY_FUND_TAG_ID,
|
||||
MAX_CHART_ITEMS,
|
||||
UNKNOWN_KEY
|
||||
@ -90,11 +90,8 @@ const europeMarkets = require('../../assets/countries/europe-markets.json');
|
||||
|
||||
@Injectable()
|
||||
export class PortfolioService {
|
||||
private baseCurrency: string;
|
||||
|
||||
public constructor(
|
||||
private readonly accountService: AccountService,
|
||||
private readonly configurationService: ConfigurationService,
|
||||
private readonly currentRateService: CurrentRateService,
|
||||
private readonly dataProviderService: DataProviderService,
|
||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||
@ -104,9 +101,7 @@ export class PortfolioService {
|
||||
private readonly rulesService: RulesService,
|
||||
private readonly symbolProfileService: SymbolProfileService,
|
||||
private readonly userService: UserService
|
||||
) {
|
||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
||||
}
|
||||
) {}
|
||||
|
||||
public async getAccounts({
|
||||
filters,
|
||||
@ -1768,7 +1763,7 @@ export class PortfolioService {
|
||||
portfolioOrders: PortfolioOrder[];
|
||||
}> {
|
||||
const userCurrency =
|
||||
this.request.user?.Settings?.settings.baseCurrency ?? this.baseCurrency;
|
||||
this.request.user?.Settings?.settings.baseCurrency ?? DEFAULT_CURRENCY;
|
||||
|
||||
const orders = await this.orderService.getOrders({
|
||||
filters,
|
||||
@ -1990,7 +1985,7 @@ export class PortfolioService {
|
||||
return (
|
||||
aUser.Settings?.settings.baseCurrency ??
|
||||
this.request.user?.Settings?.settings.baseCurrency ??
|
||||
this.baseCurrency
|
||||
DEFAULT_CURRENCY
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -19,22 +19,22 @@ import { UserWithSettings } from '@ghostfolio/common/types';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma, Role, User } from '@prisma/client';
|
||||
import { differenceInDays } from 'date-fns';
|
||||
import { sortBy } from 'lodash';
|
||||
import { sortBy, without } from 'lodash';
|
||||
|
||||
const crypto = require('crypto');
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
private baseCurrency: string;
|
||||
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService,
|
||||
private readonly prismaService: PrismaService,
|
||||
private readonly propertyService: PropertyService,
|
||||
private readonly subscriptionService: SubscriptionService,
|
||||
private readonly tagService: TagService
|
||||
) {
|
||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
||||
) {}
|
||||
|
||||
public async count(args?: Prisma.UserCountArgs) {
|
||||
return this.prismaService.user.count(args);
|
||||
}
|
||||
|
||||
public async getUser(
|
||||
@ -188,6 +188,11 @@ export class UserService {
|
||||
currentPermissions.push(permissions.enableSubscriptionInterstitial);
|
||||
}
|
||||
|
||||
currentPermissions = without(
|
||||
currentPermissions,
|
||||
permissions.createAccess
|
||||
);
|
||||
|
||||
// Reset benchmark
|
||||
user.Settings.settings.benchmark = undefined;
|
||||
}
|
||||
@ -267,7 +272,7 @@ export class UserService {
|
||||
...data,
|
||||
Account: {
|
||||
create: {
|
||||
currency: this.baseCurrency,
|
||||
currency: DEFAULT_CURRENCY,
|
||||
isDefault: true,
|
||||
name: 'Default Account'
|
||||
}
|
||||
@ -275,7 +280,7 @@ export class UserService {
|
||||
Settings: {
|
||||
create: {
|
||||
settings: {
|
||||
currency: this.baseCurrency
|
||||
currency: DEFAULT_CURRENCY
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1293,6 +1293,7 @@
|
||||
"BZKY": "Bizkey",
|
||||
"BZL": "BZLCoin",
|
||||
"BZNT": "Bezant",
|
||||
"BZR": "Bazaars",
|
||||
"BZRX": "bZx Protocol",
|
||||
"BZX": "Bitcoin Zero",
|
||||
"BZZ": "Swarmv",
|
||||
@ -2564,7 +2565,7 @@
|
||||
"ELONGT": "Elon GOAT",
|
||||
"ELONONE": "AstroElon",
|
||||
"ELP": "Ellerium",
|
||||
"ELS": "Elysium",
|
||||
"ELS": "Ethlas",
|
||||
"ELT": "Element Black",
|
||||
"ELTC2": "eLTC",
|
||||
"ELTCOIN": "ELTCOIN",
|
||||
@ -2573,6 +2574,7 @@
|
||||
"ELVN": "11Minutes",
|
||||
"ELX": "Energy Ledger",
|
||||
"ELY": "Elysian",
|
||||
"ELYSIUM": "Elysium",
|
||||
"EM": "Eminer",
|
||||
"EMANATE": "EMANATE",
|
||||
"EMAR": "EmaratCoin",
|
||||
@ -2890,6 +2892,7 @@
|
||||
"FDO": "Firdaos",
|
||||
"FDR": "French Digital Reserve",
|
||||
"FDT": "Frutti Dino",
|
||||
"FDUSD": "First Digital USD",
|
||||
"FDX": "fidentiaX",
|
||||
"FDZ": "Friendz",
|
||||
"FEAR": "Fear",
|
||||
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"CYBER24781": "CyberConnect",
|
||||
"LUNA1": "Terra",
|
||||
"LUNA2": "Terra",
|
||||
"SGB1": "Songbird",
|
||||
|
@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset
|
||||
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
|
||||
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
||||
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
|
||||
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -50,6 +50,110 @@
|
||||
<loc>https://ghostfol.io/de/ressourcen</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-altoo</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-copilot-money</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-delta</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-divvydiary</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-exirio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-folishare</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-getquin</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-gospatz</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-justetf</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-kubera</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-markets.sh</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-maybe-finance</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-monse</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-parqet</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-plannix</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-portfolio-dividend-tracker</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-portseido</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-projectionlab</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-seeking-alpha</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-sharesight</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-simple-portfolio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-snowball-analytics</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-sumio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-utluna</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-yeekatee</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ueber-uns</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -146,6 +250,10 @@
|
||||
<loc>https://ghostfol.io/en/blog/2023/08/ghostfolio-joins-oss-friends</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/blog/2023/09/ghostfolio-2</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/faq</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -442,6 +550,110 @@
|
||||
<loc>https://ghostfol.io/it/risorse</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-altoo</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-copilot-money</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-delta</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-divvydiary</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-exirio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-folishare</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-getquin</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-gospatz</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-justetf</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-kubera</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-markets.sh</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-maybe-finance</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-monse</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-parqet</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-plannix</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-portfolio-dividend-tracker</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-portseido</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-projectionlab</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-seeking-alpha</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-sharesight</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-simple-portfolio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-snowball-analytics</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-sumio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-utluna</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-yeekatee</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -451,7 +663,111 @@
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/kenmerken</loc>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-altoo</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-copilot-money</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-delta</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-divvydiary</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-exirio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-folishare</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-getquin</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-gospatz</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-justetf</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-kubera</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-markets.sh</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-maybe-finance</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-monse</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-parqet</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-plannix</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-portfolio-dividend-tracker</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-portseido</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-projectionlab</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-seeking-alpha</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-sharesight</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-simple-portfolio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-snowball-analytics</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-sumio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-utluna</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-yeekatee</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/functionaliteiten</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
@ -493,7 +809,11 @@
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/vaak-gestelde-vragen</loc>
|
||||
<loc>https://ghostfol.io/nl/veelgestelde-vragen</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/pt</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
|
@ -55,7 +55,6 @@ async function bootstrap() {
|
||||
|
||||
app.use(HtmlTemplateMiddleware);
|
||||
|
||||
const BASE_CURRENCY = configService.get<string>('BASE_CURRENCY');
|
||||
const HOST = configService.get<string>('HOST') || '0.0.0.0';
|
||||
const PORT = configService.get<number>('PORT') || 3333;
|
||||
|
||||
@ -63,15 +62,6 @@ async function bootstrap() {
|
||||
logLogo();
|
||||
Logger.log(`Listening at http://${HOST}:${PORT}`);
|
||||
Logger.log('');
|
||||
|
||||
if (BASE_CURRENCY) {
|
||||
Logger.warn(
|
||||
`The environment variable "BASE_CURRENCY" is deprecated and will be removed in Ghostfolio 2.0.`
|
||||
);
|
||||
Logger.warn(
|
||||
'Please use the currency converter in the activity dialog instead.'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -75,6 +75,10 @@ const locales = {
|
||||
'/en/blog/2023/08/ghostfolio-joins-oss-friends': {
|
||||
featureGraphicPath: 'assets/images/blog/ghostfolio-joins-oss-friends.png',
|
||||
title: `Ghostfolio joins OSS Friends - ${titleShort}`
|
||||
},
|
||||
'/en/blog/2023/09/ghostfolio-2': {
|
||||
featureGraphicPath: 'assets/images/blog/ghostfolio-2.jpg',
|
||||
title: `Announcing Ghostfolio 2.0 - ${titleShort}`
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Environment } from '@ghostfolio/api/services/interfaces/environment.interface';
|
||||
import { DEFAULT_CURRENCY, DEFAULT_ROOT_URL } from '@ghostfolio/common/config';
|
||||
import { DEFAULT_ROOT_URL } from '@ghostfolio/common/config';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource } from '@prisma/client';
|
||||
import { bool, cleanEnv, host, json, num, port, str } from 'envalid';
|
||||
@ -12,10 +12,6 @@ export class ConfigurationService {
|
||||
this.environmentConfiguration = cleanEnv(process.env, {
|
||||
ACCESS_TOKEN_SALT: str(),
|
||||
ALPHA_VANTAGE_API_KEY: str({ default: '' }),
|
||||
BASE_CURRENCY: str({
|
||||
choices: ['AUD', 'CAD', 'CNY', 'EUR', 'GBP', 'JPY', 'RUB', 'USD'],
|
||||
default: DEFAULT_CURRENCY
|
||||
}),
|
||||
BETTER_UPTIME_API_KEY: str({ default: '' }),
|
||||
CACHE_QUOTES_TTL: num({ default: 1 }),
|
||||
CACHE_TTL: num({ default: 1 }),
|
||||
|
@ -145,7 +145,9 @@ export class DataGatheringService {
|
||||
});
|
||||
} catch (error) {
|
||||
Logger.error(
|
||||
`Failed to enhance data for symbol ${symbol} by ${dataEnhancer.getName()}`,
|
||||
`Failed to enhance data for ${symbol} (${
|
||||
assetProfile.dataSource
|
||||
}) by ${dataEnhancer.getName()}`,
|
||||
error,
|
||||
'DataGatheringService'
|
||||
);
|
||||
|
@ -9,6 +9,7 @@ import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||
import { Granularity } from '@ghostfolio/common/types';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource, SymbolProfile } from '@prisma/client';
|
||||
import * as Alphavantage from 'alphavantage';
|
||||
import { format, isAfter, isBefore, parse } from 'date-fns';
|
||||
|
||||
import { IAlphaVantageHistoricalResponse } from './interfaces/interfaces';
|
||||
@ -20,7 +21,7 @@ export class AlphaVantageService implements DataProviderInterface {
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService
|
||||
) {
|
||||
this.alphaVantage = require('alphavantage')({
|
||||
this.alphaVantage = Alphavantage({
|
||||
key: this.configurationService.get('ALPHA_VANTAGE_API_KEY')
|
||||
});
|
||||
}
|
||||
@ -126,6 +127,9 @@ export class AlphaVantageService implements DataProviderInterface {
|
||||
return {
|
||||
items: result?.bestMatches?.map((bestMatch) => {
|
||||
return {
|
||||
assetClass: undefined,
|
||||
assetSubClass: undefined,
|
||||
currency: bestMatch['8. currency'],
|
||||
dataSource: this.getName(),
|
||||
name: bestMatch['2. name'],
|
||||
symbol: bestMatch['1. symbol']
|
||||
|
@ -1,10 +1,13 @@
|
||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||
import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
|
||||
import {
|
||||
IDataProviderHistoricalResponse,
|
||||
IDataProviderResponse
|
||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import {
|
||||
DEFAULT_CURRENCY,
|
||||
DEFAULT_REQUEST_TIMEOUT
|
||||
} from '@ghostfolio/common/config';
|
||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
|
||||
import { Granularity } from '@ghostfolio/common/types';
|
||||
@ -20,14 +23,9 @@ import got from 'got';
|
||||
|
||||
@Injectable()
|
||||
export class CoinGeckoService implements DataProviderInterface {
|
||||
private baseCurrency: string;
|
||||
private readonly URL = 'https://api.coingecko.com/api/v3';
|
||||
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService
|
||||
) {
|
||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
||||
}
|
||||
public constructor() {}
|
||||
|
||||
public canHandle(symbol: string) {
|
||||
return true;
|
||||
@ -39,13 +37,22 @@ export class CoinGeckoService implements DataProviderInterface {
|
||||
const response: Partial<SymbolProfile> = {
|
||||
assetClass: AssetClass.CASH,
|
||||
assetSubClass: AssetSubClass.CRYPTOCURRENCY,
|
||||
currency: this.baseCurrency,
|
||||
currency: DEFAULT_CURRENCY,
|
||||
dataSource: this.getName(),
|
||||
symbol: aSymbol
|
||||
};
|
||||
|
||||
try {
|
||||
const { name } = await got(`${this.URL}/coins/${aSymbol}`).json<any>();
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const { name } = await got(`${this.URL}/coins/${aSymbol}`, {
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}).json<any>();
|
||||
|
||||
response.name = name;
|
||||
} catch (error) {
|
||||
@ -78,12 +85,22 @@ export class CoinGeckoService implements DataProviderInterface {
|
||||
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
||||
}> {
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const { prices } = await got(
|
||||
`${
|
||||
this.URL
|
||||
}/coins/${aSymbol}/market_chart/range?vs_currency=${this.baseCurrency.toLowerCase()}&from=${getUnixTime(
|
||||
}/coins/${aSymbol}/market_chart/range?vs_currency=${DEFAULT_CURRENCY.toLowerCase()}&from=${getUnixTime(
|
||||
from
|
||||
)}&to=${getUnixTime(to)}`
|
||||
)}&to=${getUnixTime(to)}`,
|
||||
{
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
).json<any>();
|
||||
|
||||
const result: {
|
||||
@ -127,19 +144,29 @@ export class CoinGeckoService implements DataProviderInterface {
|
||||
}
|
||||
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const response = await got(
|
||||
`${this.URL}/simple/price?ids=${aSymbols.join(
|
||||
','
|
||||
)}&vs_currencies=${this.baseCurrency.toLowerCase()}`
|
||||
)}&vs_currencies=${DEFAULT_CURRENCY.toLowerCase()}`,
|
||||
{
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
).json<any>();
|
||||
|
||||
for (const symbol in response) {
|
||||
if (Object.prototype.hasOwnProperty.call(response, symbol)) {
|
||||
results[symbol] = {
|
||||
currency: this.baseCurrency,
|
||||
currency: DEFAULT_CURRENCY,
|
||||
dataProviderInfo: this.getDataProviderInfo(),
|
||||
dataSource: DataSource.COINGECKO,
|
||||
marketPrice: response[symbol][this.baseCurrency.toLowerCase()],
|
||||
marketPrice: response[symbol][DEFAULT_CURRENCY.toLowerCase()],
|
||||
marketState: 'open'
|
||||
};
|
||||
}
|
||||
@ -165,9 +192,16 @@ export class CoinGeckoService implements DataProviderInterface {
|
||||
let items: LookupItem[] = [];
|
||||
|
||||
try {
|
||||
const { coins } = await got(
|
||||
`${this.URL}/search?query=${query}`
|
||||
).json<any>();
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const { coins } = await got(`${this.URL}/search?query=${query}`, {
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}).json<any>();
|
||||
|
||||
items = coins.map(({ id: symbol, name }) => {
|
||||
return {
|
||||
@ -175,7 +209,7 @@ export class CoinGeckoService implements DataProviderInterface {
|
||||
symbol,
|
||||
assetClass: AssetClass.CASH,
|
||||
assetSubClass: AssetSubClass.CRYPTOCURRENCY,
|
||||
currency: this.baseCurrency,
|
||||
currency: DEFAULT_CURRENCY,
|
||||
dataSource: this.getName()
|
||||
};
|
||||
});
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
|
||||
import { DEFAULT_REQUEST_TIMEOUT } from '@ghostfolio/common/config';
|
||||
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
||||
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
@ -32,15 +33,35 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
||||
return response;
|
||||
}
|
||||
|
||||
let abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const profile = await got(
|
||||
`${TrackinsightDataEnhancerService.baseUrl}/funds/${symbol}.json`
|
||||
`${TrackinsightDataEnhancerService.baseUrl}/funds/${symbol}.json`,
|
||||
{
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
)
|
||||
.json<any>()
|
||||
.catch(() => {
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
return got(
|
||||
`${TrackinsightDataEnhancerService.baseUrl}/funds/${symbol.split(
|
||||
'.'
|
||||
)?.[0]}.json`
|
||||
)?.[0]}.json`,
|
||||
{
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
)
|
||||
.json<any>()
|
||||
.catch(() => {
|
||||
@ -54,15 +75,35 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
||||
response.isin = isin;
|
||||
}
|
||||
|
||||
abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const holdings = await got(
|
||||
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${symbol}.json`
|
||||
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${symbol}.json`,
|
||||
{
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
)
|
||||
.json<any>()
|
||||
.catch(() => {
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
return got(
|
||||
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${symbol.split(
|
||||
'.'
|
||||
)?.[0]}.json`
|
||||
)?.[0]}.json`,
|
||||
{
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
)
|
||||
.json<any>()
|
||||
.catch(() => {
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
|
||||
|
||||
import { YahooFinanceDataEnhancerService } from './yahoo-finance.service';
|
||||
@ -26,16 +25,13 @@ jest.mock(
|
||||
);
|
||||
|
||||
describe('YahooFinanceDataEnhancerService', () => {
|
||||
let configurationService: ConfigurationService;
|
||||
let cryptocurrencyService: CryptocurrencyService;
|
||||
let yahooFinanceDataEnhancerService: YahooFinanceDataEnhancerService;
|
||||
|
||||
beforeAll(async () => {
|
||||
configurationService = new ConfigurationService();
|
||||
cryptocurrencyService = new CryptocurrencyService();
|
||||
|
||||
yahooFinanceDataEnhancerService = new YahooFinanceDataEnhancerService(
|
||||
configurationService,
|
||||
cryptocurrencyService
|
||||
);
|
||||
});
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
|
||||
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
|
||||
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||
import { DEFAULT_CURRENCY, UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||
import { isCurrency } from '@ghostfolio/common/helper';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import {
|
||||
AssetClass,
|
||||
AssetSubClass,
|
||||
DataSource,
|
||||
Prisma,
|
||||
SymbolProfile
|
||||
} from '@prisma/client';
|
||||
import { countries } from 'countries-list';
|
||||
@ -16,23 +16,18 @@ import type { Price } from 'yahoo-finance2/dist/esm/src/modules/quoteSummary-ifa
|
||||
|
||||
@Injectable()
|
||||
export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
|
||||
private baseCurrency: string;
|
||||
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService,
|
||||
private readonly cryptocurrencyService: CryptocurrencyService
|
||||
) {
|
||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
||||
}
|
||||
) {}
|
||||
|
||||
public convertFromYahooFinanceSymbol(aYahooFinanceSymbol: string) {
|
||||
let symbol = aYahooFinanceSymbol.replace(
|
||||
new RegExp(`-${this.baseCurrency}$`),
|
||||
this.baseCurrency
|
||||
new RegExp(`-${DEFAULT_CURRENCY}$`),
|
||||
DEFAULT_CURRENCY
|
||||
);
|
||||
|
||||
if (symbol.includes('=X') && !symbol.includes(this.baseCurrency)) {
|
||||
symbol = `${this.baseCurrency}${symbol}`;
|
||||
if (symbol.includes('=X') && !symbol.includes(DEFAULT_CURRENCY)) {
|
||||
symbol = `${DEFAULT_CURRENCY}${symbol}`;
|
||||
}
|
||||
|
||||
return symbol.replace('=X', '');
|
||||
@ -47,21 +42,18 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
|
||||
*/
|
||||
public convertToYahooFinanceSymbol(aSymbol: string) {
|
||||
if (
|
||||
aSymbol.includes(this.baseCurrency) &&
|
||||
aSymbol.length > this.baseCurrency.length
|
||||
aSymbol.includes(DEFAULT_CURRENCY) &&
|
||||
aSymbol.length > DEFAULT_CURRENCY.length
|
||||
) {
|
||||
if (
|
||||
isCurrency(
|
||||
aSymbol.substring(0, aSymbol.length - this.baseCurrency.length)
|
||||
aSymbol.substring(0, aSymbol.length - DEFAULT_CURRENCY.length)
|
||||
)
|
||||
) {
|
||||
return `${aSymbol}=X`;
|
||||
} else if (
|
||||
this.cryptocurrencyService.isCryptocurrency(
|
||||
aSymbol.replace(
|
||||
new RegExp(`-${this.baseCurrency}$`),
|
||||
this.baseCurrency
|
||||
)
|
||||
aSymbol.replace(new RegExp(`-${DEFAULT_CURRENCY}$`), DEFAULT_CURRENCY)
|
||||
)
|
||||
) {
|
||||
// Add a dash before the last three characters
|
||||
@ -69,8 +61,8 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
|
||||
// DOGEUSD -> DOGE-USD
|
||||
// SOL1USD -> SOL1-USD
|
||||
return aSymbol.replace(
|
||||
new RegExp(`-?${this.baseCurrency}$`),
|
||||
`-${this.baseCurrency}`
|
||||
new RegExp(`-?${DEFAULT_CURRENCY}$`),
|
||||
`-${DEFAULT_CURRENCY}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -102,11 +94,11 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
|
||||
const { countries, sectors, url } =
|
||||
await this.getAssetProfile(yahooSymbol);
|
||||
|
||||
if (countries) {
|
||||
if ((countries as unknown as Prisma.JsonArray)?.length > 0) {
|
||||
response.countries = countries;
|
||||
}
|
||||
|
||||
if (sectors) {
|
||||
if ((sectors as unknown as Prisma.JsonArray)?.length > 0) {
|
||||
response.sectors = sectors;
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,10 @@ import {
|
||||
IDataProviderHistoricalResponse,
|
||||
IDataProviderResponse
|
||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { DEFAULT_REQUEST_TIMEOUT } from '@ghostfolio/common/config';
|
||||
import {
|
||||
DEFAULT_CURRENCY,
|
||||
DEFAULT_REQUEST_TIMEOUT
|
||||
} from '@ghostfolio/common/config';
|
||||
import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper';
|
||||
import { Granularity } from '@ghostfolio/common/types';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
@ -18,19 +21,16 @@ import {
|
||||
import Big from 'big.js';
|
||||
import { format, isToday } from 'date-fns';
|
||||
import got from 'got';
|
||||
import ms from 'ms';
|
||||
|
||||
@Injectable()
|
||||
export class EodHistoricalDataService implements DataProviderInterface {
|
||||
private apiKey: string;
|
||||
private baseCurrency: string;
|
||||
private readonly URL = 'https://eodhistoricaldata.com/api';
|
||||
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService
|
||||
) {
|
||||
this.apiKey = this.configurationService.get('EOD_HISTORICAL_DATA_API_KEY');
|
||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
||||
}
|
||||
|
||||
public canHandle(symbol: string) {
|
||||
@ -78,6 +78,12 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
const symbol = this.convertToEodSymbol(aSymbol);
|
||||
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const response = await got(
|
||||
`${this.URL}/eod/${symbol}?api_token=${
|
||||
this.apiKey
|
||||
@ -86,9 +92,8 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
DATE_FORMAT
|
||||
)}&period={aGranularity}`,
|
||||
{
|
||||
timeout: {
|
||||
request: DEFAULT_REQUEST_TIMEOUT
|
||||
}
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
).json<any>();
|
||||
|
||||
@ -138,14 +143,19 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
}
|
||||
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const realTimeResponse = await got(
|
||||
`${this.URL}/real-time/${symbols[0]}?api_token=${
|
||||
this.apiKey
|
||||
}&fmt=json&s=${symbols.join(',')}`,
|
||||
{
|
||||
timeout: {
|
||||
request: DEFAULT_REQUEST_TIMEOUT
|
||||
}
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
).json<any>();
|
||||
|
||||
@ -176,7 +186,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
})?.currency;
|
||||
|
||||
result[this.convertFromEodSymbol(code)] = {
|
||||
currency: currency ?? this.baseCurrency,
|
||||
currency: currency ?? DEFAULT_CURRENCY,
|
||||
dataSource: DataSource.EOD_HISTORICAL_DATA,
|
||||
marketPrice: close,
|
||||
marketState: isToday(new Date(timestamp * 1000)) ? 'open' : 'closed'
|
||||
@ -187,24 +197,24 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
{}
|
||||
);
|
||||
|
||||
if (response[`${this.baseCurrency}GBP`]) {
|
||||
response[`${this.baseCurrency}GBp`] = {
|
||||
...response[`${this.baseCurrency}GBP`],
|
||||
currency: `${this.baseCurrency}GBp`,
|
||||
if (response[`${DEFAULT_CURRENCY}GBP`]) {
|
||||
response[`${DEFAULT_CURRENCY}GBp`] = {
|
||||
...response[`${DEFAULT_CURRENCY}GBP`],
|
||||
currency: `${DEFAULT_CURRENCY}GBp`,
|
||||
marketPrice: this.getConvertedValue({
|
||||
symbol: `${this.baseCurrency}GBp`,
|
||||
value: response[`${this.baseCurrency}GBP`].marketPrice
|
||||
symbol: `${DEFAULT_CURRENCY}GBp`,
|
||||
value: response[`${DEFAULT_CURRENCY}GBP`].marketPrice
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
if (response[`${this.baseCurrency}ILS`]) {
|
||||
response[`${this.baseCurrency}ILA`] = {
|
||||
...response[`${this.baseCurrency}ILS`],
|
||||
currency: `${this.baseCurrency}ILA`,
|
||||
if (response[`${DEFAULT_CURRENCY}ILS`]) {
|
||||
response[`${DEFAULT_CURRENCY}ILA`] = {
|
||||
...response[`${DEFAULT_CURRENCY}ILS`],
|
||||
currency: `${DEFAULT_CURRENCY}ILA`,
|
||||
marketPrice: this.getConvertedValue({
|
||||
symbol: `${this.baseCurrency}ILA`,
|
||||
value: response[`${this.baseCurrency}ILS`].marketPrice
|
||||
symbol: `${DEFAULT_CURRENCY}ILA`,
|
||||
value: response[`${DEFAULT_CURRENCY}ILS`].marketPrice
|
||||
})
|
||||
};
|
||||
}
|
||||
@ -273,7 +283,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
if (symbol.endsWith('.FOREX')) {
|
||||
symbol = symbol.replace('GBX', 'GBp');
|
||||
symbol = symbol.replace('.FOREX', '');
|
||||
symbol = `${this.baseCurrency}${symbol}`;
|
||||
symbol = `${DEFAULT_CURRENCY}${symbol}`;
|
||||
}
|
||||
|
||||
return symbol;
|
||||
@ -286,17 +296,17 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
*/
|
||||
private convertToEodSymbol(aSymbol: string) {
|
||||
if (
|
||||
aSymbol.startsWith(this.baseCurrency) &&
|
||||
aSymbol.length > this.baseCurrency.length
|
||||
aSymbol.startsWith(DEFAULT_CURRENCY) &&
|
||||
aSymbol.length > DEFAULT_CURRENCY.length
|
||||
) {
|
||||
if (
|
||||
isCurrency(
|
||||
aSymbol.substring(0, aSymbol.length - this.baseCurrency.length)
|
||||
aSymbol.substring(0, aSymbol.length - DEFAULT_CURRENCY.length)
|
||||
)
|
||||
) {
|
||||
return `${aSymbol
|
||||
.replace('GBp', 'GBX')
|
||||
.replace(this.baseCurrency, '')}.FOREX`;
|
||||
.replace(DEFAULT_CURRENCY, '')}.FOREX`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -310,10 +320,10 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
symbol: string;
|
||||
value: number;
|
||||
}) {
|
||||
if (symbol === `${this.baseCurrency}GBp`) {
|
||||
if (symbol === `${DEFAULT_CURRENCY}GBp`) {
|
||||
// Convert GPB to GBp (pence)
|
||||
return new Big(value).mul(100).toNumber();
|
||||
} else if (symbol === `${this.baseCurrency}ILA`) {
|
||||
} else if (symbol === `${DEFAULT_CURRENCY}ILA`) {
|
||||
// Convert ILS to ILA
|
||||
return new Big(value).mul(100).toNumber();
|
||||
}
|
||||
@ -331,12 +341,17 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
let searchResult = [];
|
||||
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const response = await got(
|
||||
`${this.URL}/search/${aQuery}?api_token=${this.apiKey}`,
|
||||
{
|
||||
timeout: {
|
||||
request: DEFAULT_REQUEST_TIMEOUT
|
||||
}
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
).json<any>();
|
||||
|
||||
|
@ -5,6 +5,10 @@ import {
|
||||
IDataProviderHistoricalResponse,
|
||||
IDataProviderResponse
|
||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import {
|
||||
DEFAULT_CURRENCY,
|
||||
DEFAULT_REQUEST_TIMEOUT
|
||||
} from '@ghostfolio/common/config';
|
||||
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
||||
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
|
||||
import { Granularity } from '@ghostfolio/common/types';
|
||||
@ -16,7 +20,6 @@ import got from 'got';
|
||||
@Injectable()
|
||||
export class FinancialModelingPrepService implements DataProviderInterface {
|
||||
private apiKey: string;
|
||||
private baseCurrency: string;
|
||||
private readonly URL = 'https://financialmodelingprep.com/api/v3';
|
||||
|
||||
public constructor(
|
||||
@ -25,7 +28,6 @@ export class FinancialModelingPrepService implements DataProviderInterface {
|
||||
this.apiKey = this.configurationService.get(
|
||||
'FINANCIAL_MODELING_PREP_API_KEY'
|
||||
);
|
||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
||||
}
|
||||
|
||||
public canHandle(symbol: string) {
|
||||
@ -64,8 +66,18 @@ export class FinancialModelingPrepService implements DataProviderInterface {
|
||||
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
||||
}> {
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const { historical } = await got(
|
||||
`${this.URL}/historical-price-full/${aSymbol}?apikey=${this.apiKey}`
|
||||
`${this.URL}/historical-price-full/${aSymbol}?apikey=${this.apiKey}`,
|
||||
{
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
).json<any>();
|
||||
|
||||
const result: {
|
||||
@ -111,13 +123,23 @@ export class FinancialModelingPrepService implements DataProviderInterface {
|
||||
}
|
||||
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const response = await got(
|
||||
`${this.URL}/quote/${aSymbols.join(',')}?apikey=${this.apiKey}`
|
||||
`${this.URL}/quote/${aSymbols.join(',')}?apikey=${this.apiKey}`,
|
||||
{
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
).json<any>();
|
||||
|
||||
for (const { price, symbol } of response) {
|
||||
results[symbol] = {
|
||||
currency: this.baseCurrency,
|
||||
currency: DEFAULT_CURRENCY,
|
||||
dataProviderInfo: this.getDataProviderInfo(),
|
||||
dataSource: DataSource.FINANCIAL_MODELING_PREP,
|
||||
marketPrice: price,
|
||||
@ -145,8 +167,18 @@ export class FinancialModelingPrepService implements DataProviderInterface {
|
||||
let items: LookupItem[] = [];
|
||||
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const result = await got(
|
||||
`${this.URL}/search?query=${query}&apikey=${this.apiKey}`
|
||||
`${this.URL}/search?query=${query}&apikey=${this.apiKey}`,
|
||||
{
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
).json<any>();
|
||||
|
||||
items = result.map(({ currency, name, symbol }) => {
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
||||
import { DEFAULT_REQUEST_TIMEOUT } from '@ghostfolio/common/config';
|
||||
import {
|
||||
DATE_FORMAT,
|
||||
extractNumberFromString,
|
||||
@ -95,7 +96,17 @@ export class ManualService implements DataProviderInterface {
|
||||
return {};
|
||||
}
|
||||
|
||||
const { body } = await got(url, { headers });
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const { body } = await got(url, {
|
||||
headers,
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
});
|
||||
|
||||
const $ = cheerio.load(body);
|
||||
|
||||
|
@ -5,7 +5,10 @@ import {
|
||||
IDataProviderHistoricalResponse,
|
||||
IDataProviderResponse
|
||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { ghostfolioFearAndGreedIndexSymbol } from '@ghostfolio/common/config';
|
||||
import {
|
||||
DEFAULT_REQUEST_TIMEOUT,
|
||||
ghostfolioFearAndGreedIndexSymbol
|
||||
} from '@ghostfolio/common/config';
|
||||
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
|
||||
import { Granularity } from '@ghostfolio/common/types';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
@ -135,6 +138,12 @@ export class RapidApiService implements DataProviderInterface {
|
||||
oneYearAgo: { value: number; valueText: string };
|
||||
}> {
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
|
||||
setTimeout(() => {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const { fgi } = await got(
|
||||
`https://fear-and-greed-index.p.rapidapi.com/v1/fgi`,
|
||||
{
|
||||
@ -142,7 +151,9 @@ export class RapidApiService implements DataProviderInterface {
|
||||
useQueryString: 'true',
|
||||
'x-rapidapi-host': 'fear-and-greed-index.p.rapidapi.com',
|
||||
'x-rapidapi-key': this.configurationService.get('RAPID_API_API_KEY')
|
||||
}
|
||||
},
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
).json<any>();
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
|
||||
import { YahooFinanceDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service';
|
||||
import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
|
||||
@ -7,6 +6,7 @@ import {
|
||||
IDataProviderHistoricalResponse,
|
||||
IDataProviderResponse
|
||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
|
||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||
import { Granularity } from '@ghostfolio/common/types';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
@ -18,15 +18,10 @@ import { Quote } from 'yahoo-finance2/dist/esm/src/modules/quote';
|
||||
|
||||
@Injectable()
|
||||
export class YahooFinanceService implements DataProviderInterface {
|
||||
private baseCurrency: string;
|
||||
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService,
|
||||
private readonly cryptocurrencyService: CryptocurrencyService,
|
||||
private readonly yahooFinanceDataEnhancerService: YahooFinanceDataEnhancerService
|
||||
) {
|
||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
||||
}
|
||||
) {}
|
||||
|
||||
public canHandle(symbol: string) {
|
||||
return true;
|
||||
@ -212,50 +207,50 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
};
|
||||
|
||||
if (
|
||||
symbol === `${this.baseCurrency}GBP` &&
|
||||
yahooFinanceSymbols.includes(`${this.baseCurrency}GBp=X`)
|
||||
symbol === `${DEFAULT_CURRENCY}GBP` &&
|
||||
yahooFinanceSymbols.includes(`${DEFAULT_CURRENCY}GBp=X`)
|
||||
) {
|
||||
// Convert GPB to GBp (pence)
|
||||
response[`${this.baseCurrency}GBp`] = {
|
||||
response[`${DEFAULT_CURRENCY}GBp`] = {
|
||||
...response[symbol],
|
||||
currency: 'GBp',
|
||||
marketPrice: this.getConvertedValue({
|
||||
symbol: `${this.baseCurrency}GBp`,
|
||||
symbol: `${DEFAULT_CURRENCY}GBp`,
|
||||
value: response[symbol].marketPrice
|
||||
})
|
||||
};
|
||||
} else if (
|
||||
symbol === `${this.baseCurrency}ILS` &&
|
||||
yahooFinanceSymbols.includes(`${this.baseCurrency}ILA=X`)
|
||||
symbol === `${DEFAULT_CURRENCY}ILS` &&
|
||||
yahooFinanceSymbols.includes(`${DEFAULT_CURRENCY}ILA=X`)
|
||||
) {
|
||||
// Convert ILS to ILA
|
||||
response[`${this.baseCurrency}ILA`] = {
|
||||
response[`${DEFAULT_CURRENCY}ILA`] = {
|
||||
...response[symbol],
|
||||
currency: 'ILA',
|
||||
marketPrice: this.getConvertedValue({
|
||||
symbol: `${this.baseCurrency}ILA`,
|
||||
symbol: `${DEFAULT_CURRENCY}ILA`,
|
||||
value: response[symbol].marketPrice
|
||||
})
|
||||
};
|
||||
} else if (
|
||||
symbol === `${this.baseCurrency}ZAR` &&
|
||||
yahooFinanceSymbols.includes(`${this.baseCurrency}ZAc=X`)
|
||||
symbol === `${DEFAULT_CURRENCY}ZAR` &&
|
||||
yahooFinanceSymbols.includes(`${DEFAULT_CURRENCY}ZAc=X`)
|
||||
) {
|
||||
// Convert ZAR to ZAc (cents)
|
||||
response[`${this.baseCurrency}ZAc`] = {
|
||||
response[`${DEFAULT_CURRENCY}ZAc`] = {
|
||||
...response[symbol],
|
||||
currency: 'ZAc',
|
||||
marketPrice: this.getConvertedValue({
|
||||
symbol: `${this.baseCurrency}ZAc`,
|
||||
symbol: `${DEFAULT_CURRENCY}ZAc`,
|
||||
value: response[symbol].marketPrice
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (yahooFinanceSymbols.includes(`${this.baseCurrency}USX=X`)) {
|
||||
if (yahooFinanceSymbols.includes(`${DEFAULT_CURRENCY}USX=X`)) {
|
||||
// Convert USD to USX (cent)
|
||||
response[`${this.baseCurrency}USX`] = {
|
||||
response[`${DEFAULT_CURRENCY}USX`] = {
|
||||
currency: 'USX',
|
||||
dataSource: this.getName(),
|
||||
marketPrice: new Big(1).mul(100).toNumber(),
|
||||
@ -303,8 +298,8 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
(quoteType === 'CRYPTOCURRENCY' &&
|
||||
this.cryptocurrencyService.isCryptocurrency(
|
||||
symbol.replace(
|
||||
new RegExp(`-${this.baseCurrency}$`),
|
||||
this.baseCurrency
|
||||
new RegExp(`-${DEFAULT_CURRENCY}$`),
|
||||
DEFAULT_CURRENCY
|
||||
)
|
||||
)) ||
|
||||
quoteTypes.includes(quoteType)
|
||||
@ -314,7 +309,7 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
if (quoteType === 'CRYPTOCURRENCY') {
|
||||
// Only allow cryptocurrencies in base currency to avoid having redundancy in the database.
|
||||
// Transactions need to be converted manually to the base currency before
|
||||
return symbol.includes(this.baseCurrency);
|
||||
return symbol.includes(DEFAULT_CURRENCY);
|
||||
} else if (quoteType === 'FUTURE') {
|
||||
// Allow GC=F, but not MGC=F
|
||||
return symbol.length === 4;
|
||||
@ -373,13 +368,13 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
symbol: string;
|
||||
value: number;
|
||||
}) {
|
||||
if (symbol === `${this.baseCurrency}GBp`) {
|
||||
if (symbol === `${DEFAULT_CURRENCY}GBp`) {
|
||||
// Convert GPB to GBp (pence)
|
||||
return new Big(value).mul(100).toNumber();
|
||||
} else if (symbol === `${this.baseCurrency}ILA`) {
|
||||
} else if (symbol === `${DEFAULT_CURRENCY}ILA`) {
|
||||
// Convert ILS to ILA
|
||||
return new Big(value).mul(100).toNumber();
|
||||
} else if (symbol === `${this.baseCurrency}ZAc`) {
|
||||
} else if (symbol === `${DEFAULT_CURRENCY}ZAc`) {
|
||||
// Convert ZAR to ZAc (cents)
|
||||
return new Big(value).mul(100).toNumber();
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||
import { PROPERTY_CURRENCIES } from '@ghostfolio/common/config';
|
||||
import {
|
||||
DEFAULT_CURRENCY,
|
||||
PROPERTY_CURRENCIES
|
||||
} from '@ghostfolio/common/config';
|
||||
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { format, isToday } from 'date-fns';
|
||||
@ -12,13 +14,11 @@ import { isNumber, uniq } from 'lodash';
|
||||
|
||||
@Injectable()
|
||||
export class ExchangeRateDataService {
|
||||
private baseCurrency: string;
|
||||
private currencies: string[] = [];
|
||||
private currencyPairs: IDataGatheringItem[] = [];
|
||||
private exchangeRates: { [currencyPair: string]: number } = {};
|
||||
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService,
|
||||
private readonly dataProviderService: DataProviderService,
|
||||
private readonly marketDataService: MarketDataService,
|
||||
private readonly prismaService: PrismaService,
|
||||
@ -26,7 +26,7 @@ export class ExchangeRateDataService {
|
||||
) {}
|
||||
|
||||
public getCurrencies() {
|
||||
return this.currencies?.length > 0 ? this.currencies : [this.baseCurrency];
|
||||
return this.currencies?.length > 0 ? this.currencies : [DEFAULT_CURRENCY];
|
||||
}
|
||||
|
||||
public getCurrencyPairs() {
|
||||
@ -43,7 +43,6 @@ export class ExchangeRateDataService {
|
||||
}
|
||||
|
||||
public async initialize() {
|
||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
||||
this.currencies = await this.prepareCurrencies();
|
||||
this.currencyPairs = [];
|
||||
this.exchangeRates = {};
|
||||
@ -113,9 +112,9 @@ export class ExchangeRateDataService {
|
||||
if (!this.exchangeRates[symbol]) {
|
||||
// Not found, calculate indirectly via base currency
|
||||
this.exchangeRates[symbol] =
|
||||
resultExtended[`${currency1}${this.baseCurrency}`]?.[date]
|
||||
resultExtended[`${currency1}${DEFAULT_CURRENCY}`]?.[date]
|
||||
?.marketPrice *
|
||||
resultExtended[`${this.baseCurrency}${currency2}`]?.[date]
|
||||
resultExtended[`${DEFAULT_CURRENCY}${currency2}`]?.[date]
|
||||
?.marketPrice;
|
||||
|
||||
// Calculate the opposite direction
|
||||
@ -144,9 +143,8 @@ export class ExchangeRateDataService {
|
||||
} else {
|
||||
// Calculate indirectly via base currency
|
||||
const factor1 =
|
||||
this.exchangeRates[`${aFromCurrency}${this.baseCurrency}`];
|
||||
const factor2 =
|
||||
this.exchangeRates[`${this.baseCurrency}${aToCurrency}`];
|
||||
this.exchangeRates[`${aFromCurrency}${DEFAULT_CURRENCY}`];
|
||||
const factor2 = this.exchangeRates[`${DEFAULT_CURRENCY}${aToCurrency}`];
|
||||
|
||||
factor = factor1 * factor2;
|
||||
|
||||
@ -204,28 +202,28 @@ export class ExchangeRateDataService {
|
||||
let marketPriceBaseCurrencyToCurrency: number;
|
||||
|
||||
try {
|
||||
if (this.baseCurrency === aFromCurrency) {
|
||||
if (aFromCurrency === DEFAULT_CURRENCY) {
|
||||
marketPriceBaseCurrencyFromCurrency = 1;
|
||||
} else {
|
||||
marketPriceBaseCurrencyFromCurrency = (
|
||||
await this.marketDataService.get({
|
||||
dataSource,
|
||||
date: aDate,
|
||||
symbol: `${this.baseCurrency}${aFromCurrency}`
|
||||
symbol: `${DEFAULT_CURRENCY}${aFromCurrency}`
|
||||
})
|
||||
)?.marketPrice;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
if (this.baseCurrency === aToCurrency) {
|
||||
if (aToCurrency === DEFAULT_CURRENCY) {
|
||||
marketPriceBaseCurrencyToCurrency = 1;
|
||||
} else {
|
||||
marketPriceBaseCurrencyToCurrency = (
|
||||
await this.marketDataService.get({
|
||||
dataSource,
|
||||
date: aDate,
|
||||
symbol: `${this.baseCurrency}${aToCurrency}`
|
||||
symbol: `${DEFAULT_CURRENCY}${aToCurrency}`
|
||||
})
|
||||
)?.marketPrice;
|
||||
}
|
||||
@ -295,14 +293,14 @@ export class ExchangeRateDataService {
|
||||
private prepareCurrencyPairs(aCurrencies: string[]) {
|
||||
return aCurrencies
|
||||
.filter((currency) => {
|
||||
return currency !== this.baseCurrency;
|
||||
return currency !== DEFAULT_CURRENCY;
|
||||
})
|
||||
.map((currency) => {
|
||||
return {
|
||||
currency1: this.baseCurrency,
|
||||
currency1: DEFAULT_CURRENCY,
|
||||
currency2: currency,
|
||||
dataSource: this.dataProviderService.getDataSourceForExchangeRates(),
|
||||
symbol: `${this.baseCurrency}${currency}`
|
||||
symbol: `${DEFAULT_CURRENCY}${currency}`
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ import { CleanedEnvAccessors } from 'envalid';
|
||||
export interface Environment extends CleanedEnvAccessors {
|
||||
ACCESS_TOKEN_SALT: string;
|
||||
ALPHA_VANTAGE_API_KEY: string;
|
||||
BASE_CURRENCY: string;
|
||||
BETTER_UPTIME_API_KEY: string;
|
||||
CACHE_QUOTES_TTL: number;
|
||||
CACHE_TTL: number;
|
||||
|
@ -1,37 +1,26 @@
|
||||
<header>
|
||||
<gf-header
|
||||
class="position-fixed w-100"
|
||||
[currentRoute]="currentRoute"
|
||||
[info]="info"
|
||||
[pageTitle]="pageTitle"
|
||||
[user]="user"
|
||||
(signOut)="onSignOut()"
|
||||
></gf-header>
|
||||
</header>
|
||||
|
||||
<main role="main">
|
||||
<div
|
||||
*ngIf="canCreateAccount || (info?.systemMessage && user)"
|
||||
class="container info-message-container"
|
||||
class="info-message-container"
|
||||
>
|
||||
<div class="row">
|
||||
<div class="col-md-8 offset-md-2 text-center">
|
||||
<div class="info-message-inner-container position-fixed w-100">
|
||||
<div class="align-items-center d-flex h-100 justify-content-center">
|
||||
<a
|
||||
*ngIf="canCreateAccount"
|
||||
class="text-center"
|
||||
[routerLink]="routerLinkRegister"
|
||||
>
|
||||
<div
|
||||
class="cursor-pointer d-inline-block info-message px-3 py-2"
|
||||
class="cursor-pointer d-inline-block info-message"
|
||||
(click)="onCreateAccount()"
|
||||
>
|
||||
<span>You are using the Live Demo.</span>
|
||||
<span class="a ml-2">Create Account</span>
|
||||
<span i18n>You are using the Live Demo.</span>
|
||||
<span class="a ml-2" i18n>Create Account</span>
|
||||
</div></a
|
||||
>
|
||||
<div
|
||||
*ngIf="!canCreateAccount && info?.systemMessage && user"
|
||||
class="cursor-pointer d-inline-block info-message px-3 py-2 text-truncate"
|
||||
class="cursor-pointer d-inline-block info-message text-truncate"
|
||||
(click)="onShowSystemMessage()"
|
||||
>
|
||||
{{ info.systemMessage }}
|
||||
@ -40,6 +29,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<gf-header
|
||||
class="position-fixed w-100"
|
||||
[currentRoute]="currentRoute"
|
||||
[hasTabs]="hasTabs"
|
||||
[info]="info"
|
||||
[pageTitle]="pageTitle"
|
||||
[user]="user"
|
||||
(signOut)="onSignOut()"
|
||||
></gf-header>
|
||||
</header>
|
||||
|
||||
<main role="main">
|
||||
<router-outlet></router-outlet>
|
||||
</main>
|
||||
|
||||
|
@ -4,31 +4,47 @@
|
||||
display: block;
|
||||
min-height: 100vh;
|
||||
|
||||
&.has-info-message {
|
||||
header {
|
||||
height: calc(2 * var(--mat-toolbar-standard-height));
|
||||
|
||||
.info-message-container {
|
||||
height: var(--mat-toolbar-standard-height);
|
||||
|
||||
.info-message-inner-container {
|
||||
background-color: rgba(var(--palette-primary-500), 1);
|
||||
height: var(--mat-toolbar-standard-height);
|
||||
z-index: 999;
|
||||
|
||||
.info-message {
|
||||
color: rgba(var(--palette-foreground-text), 1);
|
||||
font-size: 80%;
|
||||
max-width: 100%;
|
||||
|
||||
.a {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
min-height: calc(100vh - 2 * var(--mat-toolbar-standard-height));
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
background-color: rgba(var(--palette-foreground-text), 0.05);
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
header {
|
||||
height: var(--mat-toolbar-standard-height);
|
||||
}
|
||||
|
||||
main {
|
||||
min-height: 100vh;
|
||||
padding-top: 5rem;
|
||||
|
||||
.info-message-container {
|
||||
height: 3.5rem;
|
||||
margin-top: -0.5rem;
|
||||
|
||||
.info-message {
|
||||
background-color: rgba(var(--palette-foreground-text), 0.05);
|
||||
border-radius: 2rem;
|
||||
font-size: 80%;
|
||||
max-width: 100%;
|
||||
|
||||
.a {
|
||||
color: rgba(var(--palette-primary-500), 1);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
min-height: calc(100vh - var(--mat-toolbar-standard-height));
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,12 +52,4 @@
|
||||
footer {
|
||||
background-color: rgba(var(--palette-foreground-text-dark), 0.05);
|
||||
}
|
||||
|
||||
main {
|
||||
.info-message-container {
|
||||
.info-message {
|
||||
background-color: rgba(var(--palette-foreground-text-dark), 0.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
HostBinding,
|
||||
Inject,
|
||||
OnDestroy,
|
||||
OnInit
|
||||
@ -28,14 +29,20 @@ import { UserService } from './services/user/user.service';
|
||||
styleUrls: ['./app.component.scss']
|
||||
})
|
||||
export class AppComponent implements OnDestroy, OnInit {
|
||||
@HostBinding('class.has-info-message') get getHasMessage() {
|
||||
return this.hasInfoMessage;
|
||||
}
|
||||
|
||||
public canCreateAccount: boolean;
|
||||
public currentRoute: string;
|
||||
public currentYear = new Date().getFullYear();
|
||||
public deviceType: string;
|
||||
public hasInfoMessage: boolean;
|
||||
public hasPermissionForBlog: boolean;
|
||||
public hasPermissionForStatistics: boolean;
|
||||
public hasPermissionForSubscription: boolean;
|
||||
public hasPermissionToAccessFearAndGreedIndex: boolean;
|
||||
public hasTabs = false;
|
||||
public info: InfoItem;
|
||||
public pageTitle: string;
|
||||
public routerLinkAbout = ['/' + $localize`about`];
|
||||
@ -103,6 +110,14 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
const urlSegments = urlSegmentGroup.segments;
|
||||
this.currentRoute = urlSegments[0].path;
|
||||
|
||||
this.hasTabs =
|
||||
(this.currentRoute === this.routerLinkAbout[0].slice(1) ||
|
||||
this.currentRoute === 'admin' ||
|
||||
this.currentRoute === 'home' ||
|
||||
this.currentRoute === 'portfolio' ||
|
||||
this.currentRoute === 'zen') &&
|
||||
this.deviceType !== 'mobile';
|
||||
|
||||
this.showFooter =
|
||||
(this.currentRoute === 'blog' ||
|
||||
this.currentRoute === this.routerLinkFaq[0].slice(1) ||
|
||||
@ -140,6 +155,12 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
permissions.createUserAccount
|
||||
);
|
||||
|
||||
this.hasInfoMessage =
|
||||
hasPermission(
|
||||
this.user?.permissions,
|
||||
permissions.createUserAccount
|
||||
) || !!this.info.systemMessage;
|
||||
|
||||
this.initializeTheme(this.user?.settings.colorScheme);
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
|
@ -1,3 +1,15 @@
|
||||
<div *ngIf="hasPermissionToCreateAccess" class="d-flex justify-content-end">
|
||||
<a
|
||||
color="primary"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[queryParams]="{ createDialog: true }"
|
||||
[routerLink]="[]"
|
||||
>
|
||||
Add Access
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<table class="gf-table w-100" mat-table [dataSource]="dataSource">
|
||||
<ng-container matColumnDef="alias">
|
||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Alias</th>
|
||||
|
@ -19,6 +19,7 @@ import { Access } from '@ghostfolio/common/interfaces';
|
||||
})
|
||||
export class AccessTableComponent implements OnChanges, OnInit {
|
||||
@Input() accesses: Access[];
|
||||
@Input() hasPermissionToCreateAccess = false;
|
||||
@Input() showActions: boolean;
|
||||
|
||||
@Output() accessDeleted = new EventEmitter<string>();
|
||||
|
@ -3,13 +3,20 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { AccessTableComponent } from './access-table.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AccessTableComponent],
|
||||
exports: [AccessTableComponent],
|
||||
imports: [CommonModule, MatButtonModule, MatMenuModule, MatTableModule],
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatButtonModule,
|
||||
MatMenuModule,
|
||||
MatTableModule,
|
||||
RouterModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfPortfolioAccessTableModule {}
|
||||
|
@ -29,13 +29,13 @@ import { AccountDetailDialogParams } from './interfaces/interfaces';
|
||||
styleUrls: ['./account-detail-dialog.component.scss']
|
||||
})
|
||||
export class AccountDetailDialog implements OnDestroy, OnInit {
|
||||
public accountType: string;
|
||||
public balance: number;
|
||||
public currency: string;
|
||||
public equity: number;
|
||||
public name: string;
|
||||
public orders: OrderWithAccount[];
|
||||
public platformName: string;
|
||||
public transactionCount: number;
|
||||
public user: User;
|
||||
public valueInBaseCurrency: number;
|
||||
|
||||
@ -65,15 +65,14 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(
|
||||
({
|
||||
accountType,
|
||||
balance,
|
||||
currency,
|
||||
name,
|
||||
Platform,
|
||||
transactionCount,
|
||||
value,
|
||||
valueInBaseCurrency
|
||||
}) => {
|
||||
this.accountType = translate(accountType);
|
||||
this.balance = balance;
|
||||
this.currency = currency;
|
||||
|
||||
@ -85,6 +84,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
||||
|
||||
this.name = name;
|
||||
this.platformName = Platform?.name ?? '-';
|
||||
this.transactionCount = transactionCount;
|
||||
this.valueInBaseCurrency = valueInBaseCurrency;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
|
@ -44,8 +44,8 @@
|
||||
>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<gf-value i18n size="medium" [value]="accountType"
|
||||
>Account Type</gf-value
|
||||
<gf-value i18n size="medium" [value]="transactionCount"
|
||||
>Activities</gf-value
|
||||
>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
|
@ -85,7 +85,7 @@
|
||||
<ng-container matColumnDef="transactions">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="px-1 text-right"
|
||||
class="justify-content-end px-1"
|
||||
mat-header-cell
|
||||
mat-sort-header="transactionCount"
|
||||
>
|
||||
@ -93,9 +93,7 @@
|
||||
<span class="d-none d-sm-block" i18n>Activities</span>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
||||
<ng-container *ngIf="element.accountType === 'SECURITIES'">{{
|
||||
element.transactionCount
|
||||
}}</ng-container>
|
||||
{{ element.transactionCount }}
|
||||
</td>
|
||||
<td *matFooterCellDef class="px-1 text-right" mat-footer-cell>
|
||||
{{ transactionCount }}
|
||||
|
@ -8,7 +8,6 @@ import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'page' },
|
||||
selector: 'gf-admin-settings',
|
||||
styleUrls: ['./admin-settings.component.scss'],
|
||||
templateUrl: './admin-settings.component.html'
|
||||
|
@ -1,14 +1,17 @@
|
||||
<mat-toolbar class="px-2">
|
||||
<mat-toolbar class="px-0">
|
||||
<ng-container *ngIf="user">
|
||||
<a
|
||||
class="align-items-center d-flex h-100 no-min-width px-2 rounded-0"
|
||||
mat-button
|
||||
[routerLink]="['/']"
|
||||
>
|
||||
<gf-logo [label]="pageTitle"></gf-logo>
|
||||
</a>
|
||||
<div class="d-flex h-100 logo-container" [ngClass]="{ filled: hasTabs }">
|
||||
<a
|
||||
class="align-items-center justify-content-start rounded-0"
|
||||
mat-button
|
||||
[ngClass]="{ 'w-100': hasTabs }"
|
||||
[routerLink]="['/']"
|
||||
>
|
||||
<gf-logo class="px-2" [label]="pageTitle"></gf-logo>
|
||||
</a>
|
||||
</div>
|
||||
<span class="spacer"></span>
|
||||
<ul class="alig-items-center d-flex list-inline m-0">
|
||||
<ul class="alig-items-center d-flex list-inline m-0 px-2">
|
||||
<li class="list-inline-item">
|
||||
<a
|
||||
class="d-none d-sm-block"
|
||||
@ -26,7 +29,7 @@
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
class="d-none d-sm-block"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
@ -39,7 +42,7 @@
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
class="d-none d-sm-block"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
@ -52,7 +55,7 @@
|
||||
</li>
|
||||
<li *ngIf="hasPermissionToAccessAdminControl" class="list-inline-item">
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
class="d-none d-sm-block"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
@ -65,7 +68,7 @@
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
class="d-none d-sm-block"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
@ -83,7 +86,7 @@
|
||||
class="list-inline-item"
|
||||
>
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
class="d-none d-sm-block"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
@ -96,7 +99,7 @@
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
class="d-none d-sm-block"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
@ -129,33 +132,37 @@
|
||||
<mat-menu #accountMenu="matMenu" xPosition="before">
|
||||
<ng-container *ngIf="user?.access?.length > 0">
|
||||
<button mat-menu-item (click)="impersonateAccount(null)">
|
||||
<ion-icon
|
||||
*ngIf="user?.access?.length > 0"
|
||||
class="mr-2"
|
||||
[name]="
|
||||
impersonationId
|
||||
? 'radio-button-off-outline'
|
||||
: 'radio-button-on-outline'
|
||||
"
|
||||
></ion-icon>
|
||||
<span i18n>Me</span>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon
|
||||
*ngIf="user?.access?.length > 0"
|
||||
class="mr-2"
|
||||
[name]="
|
||||
impersonationId
|
||||
? 'radio-button-off-outline'
|
||||
: 'radio-button-on-outline'
|
||||
"
|
||||
></ion-icon>
|
||||
<span i18n>Me</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
*ngFor="let accessItem of user?.access"
|
||||
mat-menu-item
|
||||
(click)="impersonateAccount(accessItem.id)"
|
||||
>
|
||||
<ion-icon
|
||||
class="mr-2"
|
||||
name="square-outline"
|
||||
[name]="
|
||||
accessItem.id === impersonationId
|
||||
? 'radio-button-on-outline'
|
||||
: 'radio-button-off-outline'
|
||||
"
|
||||
></ion-icon>
|
||||
<span *ngIf="accessItem.alias">{{ accessItem.alias }}</span>
|
||||
<span *ngIf="!accessItem.alias" i18n>User</span>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon
|
||||
class="mr-2"
|
||||
name="square-outline"
|
||||
[name]="
|
||||
accessItem.id === impersonationId
|
||||
? 'radio-button-on-outline'
|
||||
: 'radio-button-off-outline'
|
||||
"
|
||||
></ion-icon>
|
||||
<span *ngIf="accessItem.alias">{{ accessItem.alias }}</span>
|
||||
<span *ngIf="!accessItem.alias" i18n>User</span>
|
||||
</span>
|
||||
</button>
|
||||
<hr class="m-0" />
|
||||
</ng-container>
|
||||
@ -242,21 +249,25 @@
|
||||
</ul>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="user === null">
|
||||
<a
|
||||
class="align-items-center d-flex h-100 mx-2 no-min-width px-2 rounded-0"
|
||||
mat-button
|
||||
[routerLink]="['/']"
|
||||
>
|
||||
<gf-logo
|
||||
[label]="pageTitle"
|
||||
[showLabel]="currentRoute !== 'register'"
|
||||
></gf-logo>
|
||||
</a>
|
||||
<div class="d-flex h-100 logo-container" [ngClass]="{ filled: hasTabs }">
|
||||
<a
|
||||
class="align-items-center justify-content-start rounded-0"
|
||||
mat-button
|
||||
[ngClass]="{ 'w-100': hasTabs }"
|
||||
[routerLink]="['/']"
|
||||
>
|
||||
<gf-logo
|
||||
class="px-2"
|
||||
[label]="pageTitle"
|
||||
[showLabel]="currentRoute !== 'register'"
|
||||
></gf-logo>
|
||||
</a>
|
||||
</div>
|
||||
<span class="spacer"></span>
|
||||
<ul class="alig-items-center d-flex list-inline m-0">
|
||||
<ul class="alig-items-center d-flex list-inline m-0 px-2">
|
||||
<li class="list-inline-item">
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
class="d-none d-sm-block"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
@ -269,7 +280,7 @@
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
class="d-none d-sm-block"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
@ -282,6 +293,7 @@
|
||||
</li>
|
||||
<li *ngIf="hasPermissionForSubscription" class="list-inline-item">
|
||||
<a
|
||||
class="d-sm-block"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
@ -297,7 +309,7 @@
|
||||
class="list-inline-item"
|
||||
>
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
class="d-none d-sm-block"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
@ -317,13 +329,13 @@
|
||||
></a>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<button class="mx-1" mat-flat-button (click)="openLoginDialog()">
|
||||
<button class="d-sm-block" mat-flat-button (click)="openLoginDialog()">
|
||||
<ng-container i18n>Sign in</ng-container>
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
*ngIf="currentRoute !== 'register' && hasPermissionToCreateUser"
|
||||
class="list-inline-item"
|
||||
class="list-inline-item ml-1"
|
||||
>
|
||||
<a
|
||||
class="d-none d-sm-block"
|
||||
|
@ -7,8 +7,18 @@
|
||||
.mat-toolbar {
|
||||
background-color: var(--light-background);
|
||||
|
||||
.spacer {
|
||||
flex: 1 1 auto;
|
||||
.logo-container {
|
||||
&.filled {
|
||||
background-color: rgba(var(--palette-foreground-base), 0.02);
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
width: 14rem;
|
||||
}
|
||||
}
|
||||
|
||||
.list-inline-item {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mdc-button {
|
||||
@ -24,11 +34,21 @@
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
.mat-toolbar {
|
||||
background-color: var(--dark-background);
|
||||
|
||||
.logo-container {
|
||||
&.filled {
|
||||
background-color: rgba(var(--palette-foreground-base-dark), 0.02);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ import { catchError, takeUntil } from 'rxjs/operators';
|
||||
})
|
||||
export class HeaderComponent implements OnChanges {
|
||||
@Input() currentRoute: string;
|
||||
@Input() hasTabs: boolean;
|
||||
@Input() info: InfoItem;
|
||||
@Input() pageTitle: string;
|
||||
@Input() user: User;
|
||||
|
@ -5,6 +5,15 @@
|
||||
<gf-value class="justify-content-end" [value]="timeInMarket"></gf-value>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex-nowrap px-3 py-1 row"
|
||||
[hidden]="summary?.ordersCount === null"
|
||||
>
|
||||
<div class="flex-grow-1 ml-3 text-truncate" i18n>
|
||||
{{ summary?.ordersCount }} {summary?.ordersCount, plural, =1 {transaction}
|
||||
other {transactions}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col"><hr /></div>
|
||||
</div>
|
||||
@ -75,10 +84,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
<div class="flex-grow-1 text-truncate" i18n>
|
||||
Fees for {{ summary?.ordersCount }} {summary?.ordersCount, plural, =1
|
||||
{transaction} other {transactions}}
|
||||
</div>
|
||||
<div class="flex-grow-1 text-truncate" i18n>Fees</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<span *ngIf="summary?.fees || summary?.fees === 0" class="mr-1">-</span>
|
||||
<gf-value
|
||||
|
@ -0,0 +1,28 @@
|
||||
import { Directive, HostListener, Output, EventEmitter } from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[gfFileDrop]'
|
||||
})
|
||||
export class FileDropDirective {
|
||||
@Output() filesDropped = new EventEmitter<FileList>();
|
||||
|
||||
@HostListener('dragenter', ['$event']) onDragEnter(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
@HostListener('dragover', ['$event']) onDragOver(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
@HostListener('drop', ['$event']) onDrop(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// Prevent the browser's default behavior for handling the file drop
|
||||
event.dataTransfer.dropEffect = 'copy';
|
||||
|
||||
this.filesDropped.emit(event.dataTransfer.files);
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { FileDropDirective } from './file-drop.directive';
|
||||
|
||||
@NgModule({
|
||||
declarations: [FileDropDirective],
|
||||
exports: [FileDropDirective]
|
||||
})
|
||||
export class GfFileDropModule {}
|
@ -1,28 +1,20 @@
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
HostBinding,
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { TabConfiguration, User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page has-tabs' },
|
||||
selector: 'gf-about-page',
|
||||
styleUrls: ['./about-page.scss'],
|
||||
templateUrl: './about-page.html'
|
||||
})
|
||||
export class AboutPageComponent implements OnDestroy, OnInit {
|
||||
@HostBinding('class.with-info-message') get getHasMessage() {
|
||||
return this.hasMessage;
|
||||
}
|
||||
|
||||
public hasMessage: boolean;
|
||||
public deviceType: string;
|
||||
public hasPermissionForSubscription: boolean;
|
||||
public tabs: TabConfiguration[] = [];
|
||||
public user: User;
|
||||
@ -32,9 +24,10 @@ export class AboutPageComponent implements OnDestroy, OnInit {
|
||||
public constructor(
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
private deviceService: DeviceDetectorService,
|
||||
private userService: UserService
|
||||
) {
|
||||
const { globalPermissions, systemMessage } = this.dataService.fetchInfo();
|
||||
const { globalPermissions } = this.dataService.fetchInfo();
|
||||
|
||||
this.hasPermissionForSubscription = hasPermission(
|
||||
globalPermissions,
|
||||
@ -71,12 +64,6 @@ export class AboutPageComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
this.user = state.user;
|
||||
|
||||
this.hasMessage =
|
||||
hasPermission(
|
||||
this.user?.permissions,
|
||||
permissions.createUserAccount
|
||||
) || !!systemMessage;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
@ -88,7 +75,9 @@ export class AboutPageComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnInit() {}
|
||||
public ngOnInit() {
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
|
@ -2,7 +2,12 @@
|
||||
<router-outlet></router-outlet>
|
||||
</mat-tab-nav-panel>
|
||||
|
||||
<nav mat-align-tabs="center" mat-tab-nav-bar [tabPanel]="tabPanel">
|
||||
<nav
|
||||
mat-align-tabs="center"
|
||||
mat-tab-nav-bar
|
||||
[disablePagination]="true"
|
||||
[tabPanel]="tabPanel"
|
||||
>
|
||||
<ng-container *ngFor="let tab of tabs">
|
||||
<a
|
||||
#rla="routerLinkActive"
|
||||
@ -14,7 +19,10 @@
|
||||
[routerLink]="tab.path"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
>
|
||||
<ion-icon size="large" [name]="tab.iconName"></ion-icon>
|
||||
<ion-icon
|
||||
[name]="tab.iconName"
|
||||
[size]="deviceType === 'mobile' ? 'large': 'small'"
|
||||
></ion-icon>
|
||||
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
|
||||
</a>
|
||||
</ng-container>
|
||||
|
@ -2,27 +2,6 @@
|
||||
|
||||
:host {
|
||||
color: rgb(var(--dark-primary-text));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 5rem);
|
||||
overflow-y: auto;
|
||||
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
|
||||
::ng-deep {
|
||||
.mat-mdc-tab-link-container {
|
||||
--mat-tab-header-active-focus-indicator-color: transparent;
|
||||
--mat-tab-header-active-hover-indicator-color: transparent;
|
||||
--mdc-tab-indicator-active-indicator-color: transparent;
|
||||
|
||||
.mat-mdc-tab-link {
|
||||
&:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
|
@ -2,7 +2,6 @@ import { Component, OnDestroy } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
selector: 'gf-changelog-page',
|
||||
styleUrls: ['./changelog-page.scss'],
|
||||
templateUrl: './changelog-page.html'
|
||||
|
@ -2,7 +2,6 @@ import { Component, OnDestroy } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
selector: 'gf-license-page',
|
||||
styleUrls: ['./license-page.scss'],
|
||||
templateUrl: './license-page.html'
|
||||
|
@ -4,7 +4,6 @@ import { Subject } from 'rxjs';
|
||||
const ossFriends = require('../../../../assets/oss-friends.json');
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
selector: 'gf-oss-friends-page',
|
||||
styleUrls: ['./oss-friends-page.scss'],
|
||||
templateUrl: './oss-friends-page.html'
|
||||
|
@ -8,7 +8,6 @@ import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
selector: 'gf-about-overview-page',
|
||||
styleUrls: ['./about-overview-page.scss'],
|
||||
templateUrl: './about-overview-page.html'
|
||||
|
@ -2,7 +2,6 @@ import { Component, OnDestroy } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
selector: 'gf-privacy-policy-page',
|
||||
styleUrls: ['./privacy-policy-page.scss'],
|
||||
templateUrl: './privacy-policy-page.html'
|
||||
|
@ -10,7 +10,7 @@ import { ImpersonationStorageService } from '@ghostfolio/client/services/imperso
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { Account as AccountModel, AccountType } from '@prisma/client';
|
||||
import { Account as AccountModel } from '@prisma/client';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject, Subscription } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
@ -151,7 +151,6 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
public openUpdateAccountDialog({
|
||||
accountType,
|
||||
balance,
|
||||
comment,
|
||||
currency,
|
||||
@ -163,7 +162,6 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
||||
const dialogRef = this.dialog.open(CreateOrUpdateAccountDialog, {
|
||||
data: {
|
||||
account: {
|
||||
accountType,
|
||||
balance,
|
||||
comment,
|
||||
currency,
|
||||
@ -232,7 +230,6 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
||||
const dialogRef = this.dialog.open(CreateOrUpdateAccountDialog, {
|
||||
data: {
|
||||
account: {
|
||||
accountType: AccountType.SECURITIES,
|
||||
balance: 0,
|
||||
comment: null,
|
||||
currency: this.user?.settings?.baseCurrency,
|
||||
|
@ -8,15 +8,6 @@
|
||||
<input matInput name="name" required [(ngModel)]="data.account.name" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Type</mat-label>
|
||||
<mat-select name="type" required [(value)]="data.account.accountType">
|
||||
<mat-option i18n value="CASH">Cash</mat-option>
|
||||
<mat-option i18n value="SECURITIES">Securities</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Currency</mat-label>
|
||||
|
@ -1,30 +1,25 @@
|
||||
import { Component, HostBinding, OnDestroy, OnInit } from '@angular/core';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { TabConfiguration } from '@ghostfolio/common/interfaces';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page has-tabs' },
|
||||
selector: 'gf-admin-page',
|
||||
styleUrls: ['./admin-page.scss'],
|
||||
templateUrl: './admin-page.html'
|
||||
})
|
||||
export class AdminPageComponent implements OnDestroy, OnInit {
|
||||
@HostBinding('class.with-info-message') get getHasMessage() {
|
||||
return this.hasMessage;
|
||||
}
|
||||
|
||||
public hasMessage: boolean;
|
||||
public deviceType: string;
|
||||
public tabs: TabConfiguration[] = [];
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor(private dataService: DataService) {
|
||||
const { systemMessage } = this.dataService.fetchInfo();
|
||||
|
||||
this.hasMessage = !!systemMessage;
|
||||
}
|
||||
public constructor(private deviceService: DeviceDetectorService) {}
|
||||
|
||||
public ngOnInit() {
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
|
||||
this.tabs = [
|
||||
{
|
||||
iconName: 'reader-outline',
|
||||
|
@ -2,7 +2,12 @@
|
||||
<router-outlet></router-outlet>
|
||||
</mat-tab-nav-panel>
|
||||
|
||||
<nav mat-align-tabs="center" mat-tab-nav-bar [tabPanel]="tabPanel">
|
||||
<nav
|
||||
mat-align-tabs="center"
|
||||
mat-tab-nav-bar
|
||||
[disablePagination]="true"
|
||||
[tabPanel]="tabPanel"
|
||||
>
|
||||
<ng-container *ngFor="let tab of tabs">
|
||||
<a
|
||||
#rla="routerLinkActive"
|
||||
@ -14,7 +19,10 @@
|
||||
[routerLink]="tab.path"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
>
|
||||
<ion-icon size="large" [name]="tab.iconName"></ion-icon>
|
||||
<ion-icon
|
||||
[name]="tab.iconName"
|
||||
[size]="deviceType === 'mobile' ? 'large': 'small'"
|
||||
></ion-icon>
|
||||
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
|
||||
</a>
|
||||
</ng-container>
|
||||
|
@ -2,27 +2,6 @@
|
||||
|
||||
:host {
|
||||
color: rgb(var(--dark-primary-text));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 5rem);
|
||||
overflow-y: auto;
|
||||
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
|
||||
::ng-deep {
|
||||
.mat-mdc-tab-link-container {
|
||||
--mat-tab-header-active-focus-indicator-color: transparent;
|
||||
--mat-tab-header-active-hover-indicator-color: transparent;
|
||||
--mdc-tab-indicator-active-indicator-color: transparent;
|
||||
|
||||
.mat-mdc-tab-link {
|
||||
&:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
|
@ -0,0 +1,17 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
imports: [MatButtonModule, RouterModule],
|
||||
selector: 'gf-ghostfolio-2-page',
|
||||
standalone: true,
|
||||
templateUrl: './ghostfolio-2-page.html'
|
||||
})
|
||||
export class Ghostfolio2PageComponent {
|
||||
public routerLinkAbout = ['/' + $localize`about`];
|
||||
public routerLinkAboutChangelog = ['/' + $localize`about`, 'changelog'];
|
||||
public routerLinkFeatures = ['/' + $localize`features`];
|
||||
public routerLinkMarkets = ['/' + $localize`markets`];
|
||||
}
|
@ -0,0 +1,286 @@
|
||||
<div class="blog container">
|
||||
<div class="row">
|
||||
<div class="col-md-8 offset-md-2">
|
||||
<article>
|
||||
<div class="mb-4 text-center">
|
||||
<h1 class="mb-1">Ghostfolio 2.0</h1>
|
||||
<div class="mb-3 text-muted"><small>2023-09-09</small></div>
|
||||
<img
|
||||
alt="Ghostfolio 2.0 Teaser"
|
||||
class="border rounded w-100"
|
||||
src="../assets/images/blog/ghostfolio-2.jpg"
|
||||
title="Announcing Ghostfolio 2.0"
|
||||
/>
|
||||
</div>
|
||||
<section class="mb-4">
|
||||
<p>
|
||||
Since late 2020, when
|
||||
<a [routerLink]="routerLinkAbout">Ghostfolio</a> took shape, the
|
||||
main goal has remained the same: to simplify investment tracking
|
||||
while prioritizing user privacy and enable investors to make
|
||||
informed decisions. Our journey so far has been incredible, with
|
||||
over 300 releases since the
|
||||
<a href="../en/blog/2021/07/hello-ghostfolio">first major release</a
|
||||
>, close to 300’000 pulls on Docker Hub, and collaboration with 50+
|
||||
contributors from around the globe. Ghostfolio was recently featured
|
||||
on
|
||||
<a
|
||||
href="https://news.ycombinator.com/item?id=37337482"
|
||||
target="_blank"
|
||||
>Hacker News</a
|
||||
>, where it ranked on the front page and briefly hit the #1 spot.
|
||||
Shortly after, the project’s repository was trending on GitHub.
|
||||
These achievements emphasize the growing recognition for our project
|
||||
and the path we are on.
|
||||
</p>
|
||||
<p>
|
||||
Now, we are thrilled to present Ghostfolio 2.0, another milestone in
|
||||
our journey to empower investors.
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<h2 class="h4">Introducing Ghostfolio 2.0</h2>
|
||||
<p>
|
||||
Ghostfolio 2.0 is the evolution of our
|
||||
<a [routerLink]="routerLinkFeatures"
|
||||
>open source wealth management software</a
|
||||
>, elevating both user and developer experiences. We have extended
|
||||
data import capabilities, added comprehensive analytics, increased
|
||||
stability, and utilized the latest technology to deliver these
|
||||
improvements. Here is a closer look at a selection of the
|
||||
improvements you can expect from this
|
||||
<a [routerLink]="routerLinkAboutChangelog">release</a>, alongside
|
||||
uncounted smaller additions and enhancements.
|
||||
</p>
|
||||
<h3 class="h5">Extended Data Import Capabilities</h3>
|
||||
<p>
|
||||
Importing account activities is an important aspect of any portfolio
|
||||
management software. With Ghostfolio 2.0, we have extended our data
|
||||
import functionality, ensuring a seamless experience for users. Our
|
||||
system supports multiple formats to make the experience more
|
||||
seamless. Additionally, there is an API, providing you with even
|
||||
greater flexibility and control over how you import transactions.
|
||||
</p>
|
||||
<h3 class="h5">Comprehensive Analytics</h3>
|
||||
<p>
|
||||
Understanding your wealth is key. The latest release offers more
|
||||
comprehensive analytics to categorize your securities, providing
|
||||
invaluable insights into your investment portfolio for making
|
||||
informed decisions and optimizing diversification.
|
||||
</p>
|
||||
<h3 class="h5">Reliable Stability</h3>
|
||||
<p>
|
||||
Ensuring the stability of software is crucial, and a platform for
|
||||
managing your wealth is no exception. The increased robustness of
|
||||
our architecture means that you can count on Ghostfolio to be there
|
||||
when you need it most, no matter the
|
||||
<a [routerLink]="routerLinkMarkets">market conditions</a>.
|
||||
</p>
|
||||
<h3 class="h5">Cutting-Edge Technology Stack</h3>
|
||||
<p>
|
||||
Ghostfolio 2.0 leverages the latest tech stack to deliver a superior
|
||||
user and developer experience. We have upgraded to
|
||||
<a href="https://angular.io" target="_blank">Angular 16</a>,
|
||||
<a href="https://nestjs.com" target="_blank">Nest.js 10</a>,
|
||||
<a href="https://www.prisma.io" target="_blank">Prisma 5</a>, and
|
||||
<a href="https://nx.dev" target="_blank">Nx 16</a>, ensuring that
|
||||
the software is at the forefront of innovation. This upgrade allows
|
||||
us to provide you with the best possible user experience, making
|
||||
investment tracking more intuitive than ever before.
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<h2 class="h4">Thriving Ghostfolio Community</h2>
|
||||
<p>
|
||||
In open source software (OSS) projects like Ghostfolio, the
|
||||
community is the driving force behind its success. Without the
|
||||
incredible support of our users and contributors, it would not have
|
||||
been possible. As we celebrate the launch of Ghostfolio 2.0, we are
|
||||
delighted to showcase the growth of the Ghostfolio community:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
Ghostfolio has accumulated <strong>2500+ stars</strong> on
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio"
|
||||
target="_blank"
|
||||
title="Find Ghostfolio on GitHub"
|
||||
>GitHub</a
|
||||
>, highlighting the appreciation and adoption of our platform by
|
||||
the community.
|
||||
</li>
|
||||
<li>
|
||||
Our
|
||||
<a
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
target="_blank"
|
||||
title="Join the Ghostfolio Slack community"
|
||||
>Slack</a
|
||||
>
|
||||
community has expanded to over <strong>350 members</strong>,
|
||||
creating a space for like-minded investors to connect, share
|
||||
insights, and collaborate.
|
||||
</li>
|
||||
<li>
|
||||
On
|
||||
<a href="https://twitter.com/ghostfolio_" target="_blank">X</a>
|
||||
(formerly Twitter), over
|
||||
<strong>300 investors and personal finance enthusiasts</strong>
|
||||
follow Ghostfolio, keen to stay updated on the latest
|
||||
developments.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
This is just the beginning. Ghostfolio is dedicated to ongoing
|
||||
improvement and helping grow a vibrant community of investors. We
|
||||
invite you to join us on this exciting journey.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Join our Slack community</strong>: Connect with fellow
|
||||
investors, share your insights, and stay updated on the latest news
|
||||
by joining our
|
||||
<a
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
target="_blank"
|
||||
title="Join the Ghostfolio Slack community"
|
||||
>Slack</a
|
||||
>
|
||||
community. It is a dynamic space where you can learn, collaborate,
|
||||
and grow together with us.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Follow us on X</strong>: For release updates and market
|
||||
insights, follow
|
||||
<a href="https://twitter.com/ghostfolio_" target="_blank"
|
||||
>Ghostfolio on X</a
|
||||
>. It is the perfect place to stay informed and connect with our
|
||||
team.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Give us a Star</strong>: If you have found value in
|
||||
Ghostfolio or appreciate our commitment to simplifying investment
|
||||
tracking, please consider giving us a star on
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio"
|
||||
target="_blank"
|
||||
title="Find Ghostfolio on GitHub"
|
||||
>GitHub</a
|
||||
>. Your support helps us reach a wider audience and make a
|
||||
difference in the world of wealth management.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Become a contributor</strong>: If you are a developer
|
||||
passionate about open source projects and personal finance, we
|
||||
welcome your contributions.
|
||||
<a href="https://github.com/ghostfolio/ghostfolio" target="_blank"
|
||||
>Join our developer community</a
|
||||
>, collaborate with like-minded people, and help shape the future of
|
||||
Ghostfolio.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<p>
|
||||
Ghostfolio 2.0 represents a major step forward in our mission to
|
||||
empower investors, and we could not be more excited about the future
|
||||
of the project. Together, we can build an outstanding tool that
|
||||
makes our lives easier. Thank you for being a part of the Ghostfolio
|
||||
community.
|
||||
</p>
|
||||
<p>Thomas from Ghostfolio</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<ul class="list-inline">
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Angular</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Announcement</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Collaboration</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Community</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Contribution</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Evolution</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Fintech</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Ghostfolio</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Ghostfolio 2.0</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Investment</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Nest.js</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Nx</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Open Source</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">OSS</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Personal Finance</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Platform</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Portfolio</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Prisma</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Privacy</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Release</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Software</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Stack</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Technology</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Wealth Management</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Web3</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<a i18n [routerLink]="['/blog']">Blog</a>
|
||||
</li>
|
||||
<li
|
||||
aria-current="page"
|
||||
class="active breadcrumb-item text-truncate"
|
||||
>
|
||||
Announcing Ghostfolio 2.0
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -145,6 +145,15 @@ const routes: Routes = [
|
||||
'./2023/08/ghostfolio-joins-oss-friends/ghostfolio-joins-oss-friends-page.component'
|
||||
).then((c) => c.GhostfolioJoinsOssFriendsPageComponent),
|
||||
title: 'Ghostfolio joins OSS Friends'
|
||||
},
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
path: '2023/09/ghostfolio-2',
|
||||
loadComponent: () =>
|
||||
import('./2023/09/ghostfolio-2/ghostfolio-2-page.component').then(
|
||||
(c) => c.Ghostfolio2PageComponent
|
||||
),
|
||||
title: 'Ghostfolio 2.0'
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -8,6 +8,32 @@
|
||||
finance</small
|
||||
>
|
||||
</h1>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-content>
|
||||
<div class="container p-0">
|
||||
<div class="flex-nowrap no-gutters row">
|
||||
<a
|
||||
class="d-flex overflow-hidden w-100"
|
||||
href="../en/blog/2023/09/ghostfolio-2"
|
||||
>
|
||||
<div class="flex-grow-1 overflow-hidden">
|
||||
<div class="h6 m-0 text-truncate">
|
||||
Announcing Ghostfolio 2.0
|
||||
</div>
|
||||
<div class="d-flex text-muted">2023-09-09</div>
|
||||
</div>
|
||||
<div class="align-items-center d-flex">
|
||||
<ion-icon
|
||||
class="chevron text-muted"
|
||||
name="chevron-forward-outline"
|
||||
size="small"
|
||||
></ion-icon>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-content>
|
||||
<div class="container p-0">
|
||||
|
@ -1,28 +1,20 @@
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
HostBinding,
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { TabConfiguration, User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page has-tabs' },
|
||||
selector: 'gf-home-page',
|
||||
styleUrls: ['./home-page.scss'],
|
||||
templateUrl: './home-page.html'
|
||||
})
|
||||
export class HomePageComponent implements OnDestroy, OnInit {
|
||||
@HostBinding('class.with-info-message') get getHasMessage() {
|
||||
return this.hasMessage;
|
||||
}
|
||||
|
||||
public hasMessage: boolean;
|
||||
public deviceType: string;
|
||||
public hasPermissionToAccessFearAndGreedIndex: boolean;
|
||||
public tabs: TabConfiguration[] = [];
|
||||
public user: User;
|
||||
@ -32,9 +24,10 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
||||
public constructor(
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
private deviceService: DeviceDetectorService,
|
||||
private userService: UserService
|
||||
) {
|
||||
const { globalPermissions, systemMessage } = this.dataService.fetchInfo();
|
||||
const { globalPermissions } = this.dataService.fetchInfo();
|
||||
|
||||
this.hasPermissionToAccessFearAndGreedIndex = hasPermission(
|
||||
globalPermissions,
|
||||
@ -70,18 +63,14 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
||||
];
|
||||
this.user = state.user;
|
||||
|
||||
this.hasMessage =
|
||||
hasPermission(
|
||||
this.user?.permissions,
|
||||
permissions.createUserAccount
|
||||
) || !!systemMessage;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnInit() {}
|
||||
public ngOnInit() {
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
|
@ -2,7 +2,12 @@
|
||||
<router-outlet></router-outlet>
|
||||
</mat-tab-nav-panel>
|
||||
|
||||
<nav mat-align-tabs="center" mat-tab-nav-bar [tabPanel]="tabPanel">
|
||||
<nav
|
||||
mat-align-tabs="center"
|
||||
mat-tab-nav-bar
|
||||
[disablePagination]="true"
|
||||
[tabPanel]="tabPanel"
|
||||
>
|
||||
<ng-container *ngFor="let tab of tabs">
|
||||
<a
|
||||
#rla="routerLinkActive"
|
||||
@ -14,7 +19,10 @@
|
||||
[routerLink]="tab.path"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
>
|
||||
<ion-icon size="large" [name]="tab.iconName"></ion-icon>
|
||||
<ion-icon
|
||||
[name]="tab.iconName"
|
||||
[size]="deviceType === 'mobile' ? 'large': 'small'"
|
||||
></ion-icon>
|
||||
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
|
||||
</a>
|
||||
</ng-container>
|
||||
|
@ -2,27 +2,6 @@
|
||||
|
||||
:host {
|
||||
color: rgb(var(--dark-primary-text));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 5rem);
|
||||
overflow-y: auto;
|
||||
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
|
||||
::ng-deep {
|
||||
.mat-mdc-tab-link-container {
|
||||
--mat-tab-header-active-focus-indicator-color: transparent;
|
||||
--mat-tab-header-active-hover-indicator-color: transparent;
|
||||
--mdc-tab-indicator-active-indicator-color: transparent;
|
||||
|
||||
.mat-mdc-tab-link {
|
||||
&:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
|
@ -1,9 +1,17 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col text-center">
|
||||
<h1 class="font-weight-bold intro mt-5" i18n>
|
||||
Manage your wealth like a boss
|
||||
</h1>
|
||||
<div>
|
||||
<div class="badge badge-light badge-pill border mb-3 px-3 py-2">
|
||||
<a href="../en/blog/2023/09/ghostfolio-2"
|
||||
><span class="mr-1 text-uppercase" i18n>New</span>
|
||||
<span class="font-weight-normal">Ghostfolio 2.0</span></a
|
||||
>
|
||||
</div>
|
||||
<h1 class="font-weight-bold intro" i18n>
|
||||
Manage your wealth like a boss
|
||||
</h1>
|
||||
</div>
|
||||
<p class="lead mb-4" i18n>
|
||||
Ghostfolio is a privacy-first, open source dashboard for your personal
|
||||
finances. Break down your asset allocation, know your net worth and make
|
||||
@ -134,6 +142,14 @@
|
||||
title="DEV Community - A constructive and inclusive social network for software developers"
|
||||
></a>
|
||||
</div>
|
||||
<div class="col-md-3 d-flex justify-content-center my-1">
|
||||
<a
|
||||
class="d-block logo logo-hacker-news mask"
|
||||
href="https://news.ycombinator.com"
|
||||
target="_blank"
|
||||
title="Hacker News"
|
||||
></a>
|
||||
</div>
|
||||
<div class="col-md-3 d-flex justify-content-center my-1">
|
||||
<a
|
||||
class="d-block logo logo-openstartup"
|
||||
|
@ -53,6 +53,10 @@
|
||||
mask-image: url('/assets/images/logo-dev-community.svg');
|
||||
}
|
||||
|
||||
&.logo-hacker-news {
|
||||
mask-image: url('/assets/images/logo-hacker-news.svg');
|
||||
}
|
||||
|
||||
&.logo-openstartup {
|
||||
background-image: url('/assets/images/logo-openstartup.png');
|
||||
background-position: center;
|
||||
@ -128,6 +132,7 @@
|
||||
&.logo-agplv3,
|
||||
&.logo-alternative-to,
|
||||
&.logo-dev-community,
|
||||
&.logo-hacker-news,
|
||||
&.logo-privacy-tools,
|
||||
&.logo-reddit,
|
||||
&.logo-sackgeld,
|
||||
|
@ -32,6 +32,7 @@
|
||||
<div class="col-xs-12 col-md-4 my-2">
|
||||
<gf-value
|
||||
i18n
|
||||
i18n-subLabel
|
||||
size="large"
|
||||
subLabel="(Last 24 hours)"
|
||||
[locale]="user?.settings?.locale"
|
||||
@ -42,6 +43,7 @@
|
||||
<div class="col-xs-12 col-md-4 my-2">
|
||||
<gf-value
|
||||
i18n
|
||||
i18n-subLabel
|
||||
size="large"
|
||||
subLabel="(Last 30 days)"
|
||||
[locale]="user?.settings?.locale"
|
||||
@ -52,6 +54,7 @@
|
||||
<div class="col-xs-12 col-md-4 my-2">
|
||||
<gf-value
|
||||
i18n
|
||||
i18n-subLabel
|
||||
size="large"
|
||||
subLabel="(Last 30 days)"
|
||||
[locale]="user?.settings?.locale"
|
||||
@ -119,6 +122,7 @@
|
||||
<a class="d-block" href="https://status.ghostfol.io">
|
||||
<gf-value
|
||||
i18n
|
||||
i18n-subLabel
|
||||
size="large"
|
||||
subLabel="(Last 90 days)"
|
||||
[isPercent]="true"
|
||||
|
@ -24,7 +24,6 @@ import { ImportActivitiesDialog } from './import-activities-dialog/import-activi
|
||||
import { ImportActivitiesDialogParams } from './import-activities-dialog/interfaces/interfaces';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
selector: 'gf-activities-page',
|
||||
styleUrls: ['./activities-page.scss'],
|
||||
templateUrl: './activities-page.html'
|
||||
@ -238,9 +237,7 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
||||
const dialogRef = this.dialog.open(CreateOrUpdateActivityDialog, {
|
||||
data: {
|
||||
activity,
|
||||
accounts: this.user?.accounts?.filter((account) => {
|
||||
return account.accountType === 'SECURITIES';
|
||||
}),
|
||||
accounts: this.user?.accounts,
|
||||
user: this.user
|
||||
},
|
||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||
@ -282,9 +279,7 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
||||
|
||||
const dialogRef = this.dialog.open(CreateOrUpdateActivityDialog, {
|
||||
data: {
|
||||
accounts: this.user?.accounts?.filter((account) => {
|
||||
return account.accountType === 'SECURITIES';
|
||||
}),
|
||||
accounts: this.user?.accounts,
|
||||
activity: {
|
||||
...aActivity,
|
||||
accountId: aActivity?.accountId ?? this.defaultAccountId,
|
||||
|
@ -217,6 +217,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
||||
|
||||
if (
|
||||
this.activityForm.controls['type'].value === 'BUY' ||
|
||||
this.activityForm.controls['type'].value === 'FEE' ||
|
||||
this.activityForm.controls['type'].value === 'ITEM'
|
||||
) {
|
||||
this.total =
|
||||
@ -290,7 +291,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
||||
this.activityForm.controls['searchSymbol'].updateValueAndValidity();
|
||||
this.activityForm.controls['updateAccountBalance'].disable();
|
||||
this.activityForm.controls['updateAccountBalance'].setValue(false);
|
||||
} else if (type === 'LIABILITY') {
|
||||
} else if (type === 'FEE' || type === 'LIABILITY') {
|
||||
this.activityForm.controls['accountId'].removeValidators(
|
||||
Validators.required
|
||||
);
|
||||
@ -308,13 +309,32 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
||||
Validators.required
|
||||
);
|
||||
this.activityForm.controls['dataSource'].updateValueAndValidity();
|
||||
|
||||
if (
|
||||
type === 'FEE' &&
|
||||
this.activityForm.controls['feeInCustomCurrency'].value === 0
|
||||
) {
|
||||
this.activityForm.controls['feeInCustomCurrency'].reset();
|
||||
}
|
||||
|
||||
this.activityForm.controls['name'].setValidators(Validators.required);
|
||||
this.activityForm.controls['name'].updateValueAndValidity();
|
||||
this.activityForm.controls['quantity'].setValue(1);
|
||||
|
||||
if (type === 'FEE') {
|
||||
this.activityForm.controls['quantity'].setValue(0);
|
||||
} else if (type === 'LIABILITY') {
|
||||
this.activityForm.controls['quantity'].setValue(1);
|
||||
}
|
||||
|
||||
this.activityForm.controls['searchSymbol'].removeValidators(
|
||||
Validators.required
|
||||
);
|
||||
this.activityForm.controls['searchSymbol'].updateValueAndValidity();
|
||||
|
||||
if (type === 'FEE') {
|
||||
this.activityForm.controls['unitPriceInCustomCurrency'].setValue(0);
|
||||
}
|
||||
|
||||
this.activityForm.controls['updateAccountBalance'].disable();
|
||||
this.activityForm.controls['updateAccountBalance'].setValue(false);
|
||||
} else {
|
||||
|
@ -15,34 +15,42 @@
|
||||
>{{ typesTranslationMap[activityForm.controls['type'].value]
|
||||
}}</mat-select-trigger
|
||||
>
|
||||
<mat-option class="line-height-1" value="BUY">
|
||||
<mat-option value="BUY">
|
||||
<span><b>{{ typesTranslationMap['BUY'] }}</b></span>
|
||||
<br />
|
||||
<small class="text-muted text-nowrap" i18n
|
||||
<small class="d-block line-height-1 text-muted text-nowrap" i18n
|
||||
>Stocks, ETFs, bonds, cryptocurrencies, commodities</small
|
||||
>
|
||||
</mat-option>
|
||||
<mat-option class="line-height-1" value="DIVIDEND">
|
||||
<span><b>{{ typesTranslationMap['DIVIDEND'] }}</b></span>
|
||||
<mat-option
|
||||
*ngIf="data.user?.settings?.isExperimentalFeatures"
|
||||
value="FEE"
|
||||
>
|
||||
<span><b>{{ typesTranslationMap['FEE'] }}</b></span>
|
||||
<small class="d-block line-height-1 text-muted text-nowrap" i18n
|
||||
>One-time fee, annual account fees</small
|
||||
>
|
||||
</mat-option>
|
||||
<mat-option class="line-height-1" value="LIABILITY">
|
||||
<mat-option value="DIVIDEND">
|
||||
<span><b>{{ typesTranslationMap['DIVIDEND'] }}</b></span>
|
||||
<small class="d-block line-height-1 text-muted text-nowrap" i18n
|
||||
>Distribution of corporate earnings</small
|
||||
>
|
||||
</mat-option>
|
||||
<mat-option value="LIABILITY">
|
||||
<span><b>{{ typesTranslationMap['LIABILITY'] }}</b></span>
|
||||
<br />
|
||||
<small class="text-muted text-nowrap" i18n
|
||||
<small class="d-block line-height-1 text-muted text-nowrap" i18n
|
||||
>Mortgages, personal loans, credit cards</small
|
||||
>
|
||||
</mat-option>
|
||||
<mat-option class="line-height-1" value="SELL">
|
||||
<mat-option value="SELL">
|
||||
<span><b>{{ typesTranslationMap['SELL'] }}</b></span>
|
||||
<br />
|
||||
<small class="text-muted text-nowrap" i18n
|
||||
<small class="d-block line-height-1 text-muted text-nowrap" i18n
|
||||
>Stocks, ETFs, bonds, cryptocurrencies, commodities</small
|
||||
>
|
||||
</mat-option>
|
||||
<mat-option class="line-height-1" value="ITEM">
|
||||
<mat-option value="ITEM">
|
||||
<span><b>{{ typesTranslationMap['ITEM'] }}</b></span>
|
||||
<br />
|
||||
<small class="text-muted text-nowrap" i18n
|
||||
<small class="d-block line-height-1 text-muted text-nowrap" i18n
|
||||
>Luxury items, real estate, private companies</small
|
||||
>
|
||||
</mat-option>
|
||||
@ -125,60 +133,71 @@
|
||||
</div>
|
||||
<div
|
||||
class="mb-3"
|
||||
[ngClass]="{ 'd-none': activityForm.controls['type']?.value === 'ITEM' || activityForm.controls['type']?.value === 'LIABILITY' }"
|
||||
[ngClass]="{ 'd-none': activityForm.controls['type']?.value === 'FEE' || activityForm.controls['type']?.value === 'ITEM' || activityForm.controls['type']?.value === 'LIABILITY' }"
|
||||
>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Quantity</mat-label>
|
||||
<input formControlName="quantity" matInput type="number" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="align-items-start d-flex mb-3">
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label
|
||||
><ng-container [ngSwitch]="activityForm.controls['type']?.value">
|
||||
<ng-container *ngSwitchCase="'DIVIDEND'" i18n
|
||||
>Dividend</ng-container
|
||||
>
|
||||
<ng-container *ngSwitchCase="'ITEM'" i18n>Value</ng-container>
|
||||
<ng-container *ngSwitchCase="'LIABILITY'" i18n>Value</ng-container>
|
||||
<ng-container *ngSwitchDefault i18n>Unit Price</ng-container>
|
||||
</ng-container>
|
||||
</mat-label>
|
||||
<input
|
||||
formControlName="unitPriceInCustomCurrency"
|
||||
matInput
|
||||
type="number"
|
||||
/>
|
||||
<div
|
||||
class="ml-2"
|
||||
matTextSuffix
|
||||
[ngClass]="{ 'd-none': !activityForm.controls['currency']?.value }"
|
||||
>
|
||||
<mat-select formControlName="currencyOfUnitPrice">
|
||||
<mat-option *ngFor="let currency of currencies" [value]="currency">
|
||||
{{ currency }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</div>
|
||||
<mat-error
|
||||
*ngIf="activityForm.controls['unitPriceInCustomCurrency'].hasError('invalid')"
|
||||
><ng-container i18n
|
||||
>Oops! Could not get the historical exchange rate from</ng-container
|
||||
<div
|
||||
class="mb-3"
|
||||
[ngClass]="{ 'd-none': activityForm.controls['type']?.value === 'FEE' }"
|
||||
>
|
||||
<div class="align-items-start d-flex">
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label
|
||||
><ng-container [ngSwitch]="activityForm.controls['type']?.value">
|
||||
<ng-container *ngSwitchCase="'DIVIDEND'" i18n
|
||||
>Dividend</ng-container
|
||||
>
|
||||
<ng-container *ngSwitchCase="'ITEM'" i18n>Value</ng-container>
|
||||
<ng-container *ngSwitchCase="'LIABILITY'" i18n
|
||||
>Value</ng-container
|
||||
>
|
||||
<ng-container *ngSwitchDefault i18n>Unit Price</ng-container>
|
||||
</ng-container>
|
||||
</mat-label>
|
||||
<input
|
||||
formControlName="unitPriceInCustomCurrency"
|
||||
matInput
|
||||
type="number"
|
||||
/>
|
||||
<div
|
||||
class="ml-2"
|
||||
matTextSuffix
|
||||
[ngClass]="{ 'd-none': !activityForm.controls['currency']?.value }"
|
||||
>
|
||||
{{ activityForm.controls['date']?.value | date: defaultDateFormat
|
||||
}}</mat-error
|
||||
<mat-select formControlName="currencyOfUnitPrice">
|
||||
<mat-option
|
||||
*ngFor="let currency of currencies"
|
||||
[value]="currency"
|
||||
>
|
||||
{{ currency }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</div>
|
||||
<mat-error
|
||||
*ngIf="activityForm.controls['unitPriceInCustomCurrency'].hasError('invalid')"
|
||||
><ng-container i18n
|
||||
>Oops! Could not get the historical exchange rate
|
||||
from</ng-container
|
||||
>
|
||||
{{ activityForm.controls['date']?.value | date: defaultDateFormat
|
||||
}}</mat-error
|
||||
>
|
||||
</mat-form-field>
|
||||
<button
|
||||
*ngIf="currentMarketPrice && (data.activity.type === 'BUY' || data.activity.type === 'SELL')"
|
||||
class="ml-2 mt-1 no-min-width"
|
||||
mat-button
|
||||
title="Apply current market price"
|
||||
type="button"
|
||||
(click)="applyCurrentMarketPrice()"
|
||||
>
|
||||
</mat-form-field>
|
||||
<button
|
||||
*ngIf="currentMarketPrice && (data.activity.type === 'BUY' || data.activity.type === 'SELL')"
|
||||
class="ml-2 mt-1 no-min-width"
|
||||
mat-button
|
||||
title="Apply current market price"
|
||||
type="button"
|
||||
(click)="applyCurrentMarketPrice()"
|
||||
>
|
||||
<ion-icon class="text-muted" name="refresh-outline"></ion-icon>
|
||||
</button>
|
||||
<ion-icon class="text-muted" name="refresh-outline"></ion-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-none">
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
@ -187,6 +206,7 @@
|
||||
<ng-container *ngSwitchCase="'DIVIDEND'" i18n
|
||||
>Dividend</ng-container
|
||||
>
|
||||
<ng-container *ngSwitchCase="'FEE'" i18n>Value</ng-container>
|
||||
<ng-container *ngSwitchCase="'ITEM'" i18n>Value</ng-container>
|
||||
<ng-container *ngSwitchCase="'LIABILITY'" i18n>Value</ng-container>
|
||||
<ng-container *ngSwitchDefault i18n>Unit Price</ng-container>
|
||||
|
@ -137,6 +137,20 @@ export class ImportActivitiesDialog implements OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
public onFilesDropped({
|
||||
files,
|
||||
stepper
|
||||
}: {
|
||||
files: FileList;
|
||||
stepper: MatStepper;
|
||||
}): void {
|
||||
if (files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.handleFile({ stepper, file: files[0] });
|
||||
}
|
||||
|
||||
public onImportStepChange(event: StepperSelectionEvent) {
|
||||
if (event.selectedIndex === ImportStep.UPLOAD_FILE) {
|
||||
this.importStep = ImportStep.UPLOAD_FILE;
|
||||
@ -175,97 +189,15 @@ export class ImportActivitiesDialog implements OnDestroy {
|
||||
aStepper.reset();
|
||||
}
|
||||
|
||||
public onSelectFile(aStepper: MatStepper) {
|
||||
public onSelectFile(stepper: MatStepper) {
|
||||
const input = document.createElement('input');
|
||||
input.accept = 'application/JSON, .csv';
|
||||
input.type = 'file';
|
||||
|
||||
input.onchange = (event) => {
|
||||
this.snackBar.open('⏳ ' + $localize`Validating data...`);
|
||||
|
||||
// Getting the file reference
|
||||
const file = (event.target as HTMLInputElement).files[0];
|
||||
|
||||
// Setting up the reader
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(file, 'UTF-8');
|
||||
|
||||
reader.onload = async (readerEvent) => {
|
||||
const fileContent = readerEvent.target.result as string;
|
||||
|
||||
try {
|
||||
if (file.name.endsWith('.json')) {
|
||||
const content = JSON.parse(fileContent);
|
||||
|
||||
this.accounts = content.accounts;
|
||||
|
||||
if (!isArray(content.activities)) {
|
||||
if (isArray(content.orders)) {
|
||||
this.handleImportError({
|
||||
activities: [],
|
||||
error: {
|
||||
error: {
|
||||
message: [`orders needs to be renamed to activities`]
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const { activities } =
|
||||
await this.importActivitiesService.importJson({
|
||||
accounts: content.accounts,
|
||||
activities: content.activities,
|
||||
isDryRun: true
|
||||
});
|
||||
this.activities = activities;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.handleImportError({ error, activities: content.activities });
|
||||
}
|
||||
|
||||
return;
|
||||
} else if (file.name.endsWith('.csv')) {
|
||||
try {
|
||||
const data = await this.importActivitiesService.importCsv({
|
||||
fileContent,
|
||||
isDryRun: true,
|
||||
userAccounts: this.data.user.accounts
|
||||
});
|
||||
this.activities = data.activities;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.handleImportError({
|
||||
activities: error?.activities ?? [],
|
||||
error: {
|
||||
error: { message: error?.error?.message ?? [error?.message] }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.handleImportError({
|
||||
activities: [],
|
||||
error: { error: { message: ['Unexpected format'] } }
|
||||
});
|
||||
} finally {
|
||||
this.importStep = ImportStep.SELECT_ACTIVITIES;
|
||||
this.snackBar.dismiss();
|
||||
|
||||
aStepper.next();
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
};
|
||||
this.handleFile({ file, stepper });
|
||||
};
|
||||
|
||||
input.click();
|
||||
@ -282,6 +214,97 @@ export class ImportActivitiesDialog implements OnDestroy {
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
private async handleFile({
|
||||
file,
|
||||
stepper
|
||||
}: {
|
||||
file: File;
|
||||
stepper: MatStepper;
|
||||
}): Promise<void> {
|
||||
this.snackBar.open('⏳ ' + $localize`Validating data...`);
|
||||
|
||||
// Setting up the reader
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(file, 'UTF-8');
|
||||
|
||||
reader.onload = async (readerEvent) => {
|
||||
const fileContent = readerEvent.target.result as string;
|
||||
|
||||
try {
|
||||
if (file.name.endsWith('.json')) {
|
||||
const content = JSON.parse(fileContent);
|
||||
|
||||
this.accounts = content.accounts;
|
||||
|
||||
if (!isArray(content.activities)) {
|
||||
if (isArray(content.orders)) {
|
||||
this.handleImportError({
|
||||
activities: [],
|
||||
error: {
|
||||
error: {
|
||||
message: [`orders needs to be renamed to activities`]
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const { activities } =
|
||||
await this.importActivitiesService.importJson({
|
||||
accounts: content.accounts,
|
||||
activities: content.activities,
|
||||
isDryRun: true
|
||||
});
|
||||
this.activities = activities;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.handleImportError({ error, activities: content.activities });
|
||||
}
|
||||
|
||||
return;
|
||||
} else if (file.name.endsWith('.csv')) {
|
||||
try {
|
||||
const data = await this.importActivitiesService.importCsv({
|
||||
fileContent,
|
||||
isDryRun: true,
|
||||
userAccounts: this.data.user.accounts
|
||||
});
|
||||
this.activities = data.activities;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.handleImportError({
|
||||
activities: error?.activities ?? [],
|
||||
error: {
|
||||
error: { message: error?.error?.message ?? [error?.message] }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.handleImportError({
|
||||
activities: [],
|
||||
error: { error: { message: ['Unexpected format'] } }
|
||||
});
|
||||
} finally {
|
||||
this.importStep = ImportStep.SELECT_ACTIVITIES;
|
||||
this.snackBar.dismiss();
|
||||
|
||||
stepper.next();
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private handleImportError({
|
||||
activities,
|
||||
error
|
||||
|
@ -70,29 +70,38 @@
|
||||
<ng-template #selectFile>
|
||||
<div class="d-flex flex-column justify-content-center">
|
||||
<button
|
||||
class="py-4"
|
||||
color="primary"
|
||||
mat-stroked-button
|
||||
class="drop-area p-4 text-center text-muted"
|
||||
gfFileDrop
|
||||
(click)="onSelectFile(stepper)"
|
||||
(filesDropped)="onFilesDropped({stepper, files: $event})"
|
||||
>
|
||||
<ion-icon class="mr-2" name="cloud-upload-outline"></ion-icon>
|
||||
<span i18n>Choose File</span>
|
||||
<div
|
||||
class="align-items-center d-flex flex-column justify-content-center"
|
||||
>
|
||||
<ion-icon
|
||||
class="cloud-icon"
|
||||
name="cloud-upload-outline"
|
||||
></ion-icon>
|
||||
<span i18n>Choose or drop a file here</span>
|
||||
</div>
|
||||
</button>
|
||||
<p class="mb-0 mt-4 text-center">
|
||||
<span class="mr-1" i18n
|
||||
>The following file formats are supported:</span
|
||||
>
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.csv"
|
||||
target="_blank"
|
||||
>CSV</a
|
||||
>
|
||||
<span class="mx-1" i18n>or</span>
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.json"
|
||||
target="_blank"
|
||||
>JSON</a
|
||||
>
|
||||
<p class="mb-0 mt-3 text-center">
|
||||
<small>
|
||||
<span class="mr-1" i18n
|
||||
>The following file formats are supported:</span
|
||||
>
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.csv"
|
||||
target="_blank"
|
||||
>CSV</a
|
||||
>
|
||||
<span class="mx-1" i18n>or</span>
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.json"
|
||||
target="_blank"
|
||||
>JSON</a
|
||||
>
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
</ng-template>
|
||||
@ -109,7 +118,7 @@
|
||||
>
|
||||
</ng-template>
|
||||
<div class="pt-3">
|
||||
<ng-container *ngIf="errorMessages.length === 0; else errorMessage">
|
||||
<ng-container *ngIf="errorMessages?.length === 0; else errorMessage">
|
||||
<gf-activities-table
|
||||
*ngIf="importStep === 1"
|
||||
[activities]="activities"
|
||||
|
@ -10,6 +10,7 @@ import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatStepperModule } from '@angular/material/stepper';
|
||||
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
|
||||
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
|
||||
import { GfFileDropModule } from '@ghostfolio/client/directives/file-drop/file-drop.module';
|
||||
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
|
||||
|
||||
@ -23,6 +24,7 @@ import { ImportActivitiesDialog } from './import-activities-dialog.component';
|
||||
GfActivitiesTableModule,
|
||||
GfDialogFooterModule,
|
||||
GfDialogHeaderModule,
|
||||
GfFileDropModule,
|
||||
GfSymbolModule,
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
|
@ -32,4 +32,32 @@
|
||||
right: 1.5rem;
|
||||
top: calc(50% - 10px);
|
||||
}
|
||||
|
||||
.drop-area {
|
||||
background-color: rgba(var(--palette-foreground-base), 0.02);
|
||||
border: 1px dashed
|
||||
rgba(
|
||||
var(--palette-foreground-divider),
|
||||
var(--palette-foreground-divider-alpha)
|
||||
);
|
||||
border-radius: 0.25rem;
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(var(--palette-primary-500), 1) !important;
|
||||
color: rgba(var(--palette-primary-500), 1);
|
||||
}
|
||||
|
||||
.cloud-icon {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
.drop-area {
|
||||
border-color: rgba(
|
||||
var(--palette-foreground-divider-dark),
|
||||
var(--palette-foreground-divider-alpha-dark)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,6 @@ import { Subject } from 'rxjs';
|
||||
import { distinctUntilChanged, switchMap, takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
selector: 'gf-allocations-page',
|
||||
styleUrls: ['./allocations-page.scss'],
|
||||
templateUrl: './allocations-page.html'
|
||||
@ -173,17 +172,15 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
|
||||
const accountFilters: Filter[] = this.user.accounts
|
||||
.filter(({ accountType }) => {
|
||||
return accountType === 'SECURITIES';
|
||||
})
|
||||
.map(({ id, name }) => {
|
||||
const accountFilters: Filter[] = this.user.accounts.map(
|
||||
({ id, name }) => {
|
||||
return {
|
||||
id,
|
||||
label: name,
|
||||
type: 'ACCOUNT'
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const assetClassFilters: Filter[] = [];
|
||||
for (const assetClass of Object.keys(AssetClass)) {
|
||||
|
@ -26,7 +26,6 @@ import { Subject } from 'rxjs';
|
||||
import { distinctUntilChanged, map, takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
selector: 'gf-analysis-page',
|
||||
styleUrls: ['./analysis-page.scss'],
|
||||
templateUrl: './analysis-page.html'
|
||||
@ -139,17 +138,15 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
|
||||
const accountFilters: Filter[] = this.user.accounts
|
||||
.filter(({ accountType }) => {
|
||||
return accountType === 'SECURITIES';
|
||||
})
|
||||
.map(({ id, name }) => {
|
||||
const accountFilters: Filter[] = this.user.accounts.map(
|
||||
({ id, name }) => {
|
||||
return {
|
||||
id,
|
||||
label: name,
|
||||
type: 'ACCOUNT'
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const assetClassFilters: Filter[] = [];
|
||||
for (const assetClass of Object.keys(AssetClass)) {
|
||||
|
@ -10,7 +10,6 @@ import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
selector: 'gf-fire-page',
|
||||
styleUrls: ['./fire-page.scss'],
|
||||
templateUrl: './fire-page.html'
|
||||
|
@ -20,7 +20,6 @@ import { Subject } from 'rxjs';
|
||||
import { distinctUntilChanged, switchMap, takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
selector: 'gf-holdings-page',
|
||||
styleUrls: ['./holdings-page.scss'],
|
||||
templateUrl: './holdings-page.html'
|
||||
@ -114,17 +113,15 @@ export class HoldingsPageComponent implements OnDestroy, OnInit {
|
||||
permissions.createOrder
|
||||
);
|
||||
|
||||
const accountFilters: Filter[] = this.user.accounts
|
||||
.filter(({ accountType }) => {
|
||||
return accountType === 'SECURITIES';
|
||||
})
|
||||
.map(({ id, name }) => {
|
||||
const accountFilters: Filter[] = this.user.accounts.map(
|
||||
({ id, name }) => {
|
||||
return {
|
||||
id,
|
||||
label: name,
|
||||
type: 'ACCOUNT'
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const assetClassFilters: Filter[] = [];
|
||||
for (const assetClass of Object.keys(AssetClass)) {
|
||||
|
@ -1,33 +1,18 @@
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
HostBinding,
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import {
|
||||
InfoItem,
|
||||
TabConfiguration,
|
||||
User
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { TabConfiguration, User } from '@ghostfolio/common/interfaces';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page has-tabs' },
|
||||
selector: 'gf-portfolio-page',
|
||||
styleUrls: ['./portfolio-page.scss'],
|
||||
templateUrl: './portfolio-page.html'
|
||||
})
|
||||
export class PortfolioPageComponent implements OnDestroy, OnInit {
|
||||
@HostBinding('class.with-info-message') get getHasMessage() {
|
||||
return this.hasMessage;
|
||||
}
|
||||
|
||||
public hasMessage: boolean;
|
||||
public info: InfoItem;
|
||||
public deviceType: string;
|
||||
public tabs: TabConfiguration[] = [];
|
||||
public user: User;
|
||||
|
||||
@ -35,11 +20,9 @@ export class PortfolioPageComponent implements OnDestroy, OnInit {
|
||||
|
||||
public constructor(
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
private deviceService: DeviceDetectorService,
|
||||
private userService: UserService
|
||||
) {
|
||||
this.info = this.dataService.fetchInfo();
|
||||
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((state) => {
|
||||
@ -73,18 +56,14 @@ export class PortfolioPageComponent implements OnDestroy, OnInit {
|
||||
];
|
||||
this.user = state.user;
|
||||
|
||||
this.hasMessage =
|
||||
hasPermission(
|
||||
this.user?.permissions,
|
||||
permissions.createUserAccount
|
||||
) || !!this.info.systemMessage;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnInit() {}
|
||||
public ngOnInit() {
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
|
@ -2,7 +2,12 @@
|
||||
<router-outlet></router-outlet>
|
||||
</mat-tab-nav-panel>
|
||||
|
||||
<nav mat-align-tabs="center" mat-tab-nav-bar [tabPanel]="tabPanel">
|
||||
<nav
|
||||
mat-align-tabs="center"
|
||||
mat-tab-nav-bar
|
||||
[disablePagination]="true"
|
||||
[tabPanel]="tabPanel"
|
||||
>
|
||||
<ng-container *ngFor="let tab of tabs">
|
||||
<a
|
||||
#rla="routerLinkActive"
|
||||
@ -14,7 +19,10 @@
|
||||
[routerLink]="tab.path"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
>
|
||||
<ion-icon size="large" [name]="tab.iconName"></ion-icon>
|
||||
<ion-icon
|
||||
[name]="tab.iconName"
|
||||
[size]="deviceType === 'mobile' ? 'large': 'small'"
|
||||
></ion-icon>
|
||||
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
|
||||
</a>
|
||||
</ng-container>
|
||||
|
@ -2,27 +2,6 @@
|
||||
|
||||
:host {
|
||||
color: rgb(var(--dark-primary-text));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 5rem);
|
||||
overflow-y: auto;
|
||||
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
|
||||
::ng-deep {
|
||||
.mat-mdc-tab-link-container {
|
||||
--mat-tab-header-active-focus-indicator-color: transparent;
|
||||
--mat-tab-header-active-hover-indicator-color: transparent;
|
||||
--mdc-tab-indicator-active-indicator-color: transparent;
|
||||
|
||||
.mat-mdc-tab-link {
|
||||
&:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
|
@ -2,6 +2,7 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { translate } from '@ghostfolio/ui/i18n';
|
||||
import { StripeService } from 'ngx-stripe';
|
||||
import { Subject } from 'rxjs';
|
||||
@ -17,6 +18,7 @@ export class PricingPageComponent implements OnDestroy, OnInit {
|
||||
public baseCurrency: string;
|
||||
public coupon: number;
|
||||
public couponId: string;
|
||||
public hasPermissionToUpdateUserSettings: boolean;
|
||||
public importAndExportTooltipBasic = translate(
|
||||
'DATA_IMPORT_AND_EXPORT_TOOLTIP_BASIC'
|
||||
);
|
||||
@ -55,6 +57,11 @@ export class PricingPageComponent implements OnDestroy, OnInit {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
|
||||
this.hasPermissionToUpdateUserSettings = hasPermission(
|
||||
this.user.permissions,
|
||||
permissions.updateUserSettings
|
||||
);
|
||||
|
||||
this.coupon = subscriptions?.[this.user?.subscription?.offer]?.coupon;
|
||||
this.couponId =
|
||||
subscriptions?.[this.user.subscription.offer]?.couponId;
|
||||
|
@ -329,11 +329,11 @@
|
||||
>{{ baseCurrency }} <strong
|
||||
>{{ price }}</strong
|
||||
></ng-container
|
||||
> <span>per year</span></span
|
||||
> <span i18n>per year</span></span
|
||||
>
|
||||
</p>
|
||||
<div
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
*ngIf="hasPermissionToUpdateUserSettings && user?.subscription?.type === 'Basic'"
|
||||
class="mt-3 text-center"
|
||||
>
|
||||
<button color="primary" mat-flat-button (click)="onCheckout()">
|
||||
|
@ -4,7 +4,6 @@ import { Router } from '@angular/router';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { InternetIdentityService } from '@ghostfolio/client/services/internet-identity.service';
|
||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { InfoItem, LineChartItem } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { Role } from '@prisma/client';
|
||||
@ -36,8 +35,7 @@ export class RegisterPageComponent implements OnDestroy, OnInit {
|
||||
private dialog: MatDialog,
|
||||
private internetIdentityService: InternetIdentityService,
|
||||
private router: Router,
|
||||
private tokenStorageService: TokenStorageService,
|
||||
private userService: UserService
|
||||
private tokenStorageService: TokenStorageService
|
||||
) {
|
||||
this.info = this.dataService.fetchInfo();
|
||||
|
||||
|
@ -28,6 +28,7 @@
|
||||
<ng-container *ngIf="hasPermissionForSocialLogin">
|
||||
<div class="my-3 text-muted" i18n>or</div>
|
||||
<button
|
||||
*ngIf="false"
|
||||
class="d-block mb-2 px-4 rounded-pill"
|
||||
mat-stroked-button
|
||||
(click)="onLoginWithInternetIdentity()"
|
||||
|
@ -19,7 +19,7 @@ const routes: Routes = [
|
||||
.map(({ component, key, name }) => {
|
||||
return {
|
||||
canActivate: [AuthGuard],
|
||||
path: `open-source-alternative-to-${key}`,
|
||||
path: $localize`open-source-alternative-to` + `-${key}`,
|
||||
loadComponent: () =>
|
||||
import(`./products/${key}-page.component`).then(() => component),
|
||||
title: $localize`Open Source Alternative to ${name}`
|
||||
|
@ -10,6 +10,7 @@ import { products } from './products';
|
||||
templateUrl: './personal-finance-tools-page.html'
|
||||
})
|
||||
export class PersonalFinanceToolsPageComponent implements OnDestroy {
|
||||
public pathAlternativeTo = $localize`open-source-alternative-to` + '-';
|
||||
public pathResources = '/' + $localize`resources`;
|
||||
public products = products.filter(({ key }) => {
|
||||
return key !== 'ghostfolio';
|
||||
|
@ -29,7 +29,7 @@
|
||||
<a
|
||||
class="d-flex overflow-hidden w-100"
|
||||
title="Compare Ghostfolio to {{ product.name }}"
|
||||
[routerLink]="[pathResources, 'personal-finance-tools', 'open-source-alternative-to-' + product.key]"
|
||||
[routerLink]="[pathResources, 'personal-finance-tools', pathAlternativeTo + product.key]"
|
||||
>
|
||||
<div class="flex-grow-1 overflow-hidden">
|
||||
<div class="h6 m-0 text-truncate" i18n>
|
||||
|
@ -10,7 +10,7 @@
|
||||
</h1>
|
||||
</div>
|
||||
<section class="mb-4">
|
||||
<p>
|
||||
<p i18n>
|
||||
Are you looking for an open source alternative to {{ product2.name
|
||||
}}? <a [routerLink]="routerLinkAbout">Ghostfolio</a> is a powerful
|
||||
portfolio management tool that provides individuals with a
|
||||
@ -23,7 +23,7 @@
|
||||
to help you make informed decisions and take control of your
|
||||
financial future.
|
||||
</p>
|
||||
<p>
|
||||
<p i18n>
|
||||
Ghostfolio is an open source software (OSS), providing a
|
||||
cost-effective alternative to {{ product2.name }} making it
|
||||
particularly suitable for individuals on a tight budget, such as
|
||||
@ -34,7 +34,7 @@
|
||||
and personal finance enthusiasts, Ghostfolio continuously enhances
|
||||
its capabilities, security, and user experience.
|
||||
</p>
|
||||
<p>
|
||||
<p i18n>
|
||||
Let’s dive deeper into the detailed comparison table below to gain a
|
||||
thorough understanding of how Ghostfolio positions itself relative
|
||||
to {{ product2.name }}. We will explore various aspects such as
|
||||
@ -177,11 +177,11 @@
|
||||
</tr>
|
||||
<tr class="mat-mdc-row">
|
||||
<td class="mat-mdc-cell px-3 py-2 text-right" i18n>Pricing</td>
|
||||
<td class="mat-mdc-cell px-1 py-2">
|
||||
<td class="mat-mdc-cell px-1 py-2" i18n>
|
||||
Starting from {{ product1.pricingPerYear }} / year
|
||||
</td>
|
||||
<td class="mat-mdc-cell px-1 py-2">
|
||||
<ng-container *ngIf="product2.pricingPerYear"
|
||||
<ng-container *ngIf="product2.pricingPerYear" i18n
|
||||
>Starting from {{ product2.pricingPerYear }} /
|
||||
year</ng-container
|
||||
>
|
||||
@ -196,7 +196,7 @@
|
||||
</table>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<p>
|
||||
<p i18n>
|
||||
Please note that the information provided is based on our
|
||||
independent research and analysis. This website is not affiliated
|
||||
with {{ product2.name }} or any other product mentioned in the
|
||||
@ -208,7 +208,7 @@
|
||||
</p>
|
||||
</section>
|
||||
<section class="call-to-action mb-4 py-3 rounded">
|
||||
<h2 class="h4 mb-0 text-center">
|
||||
<h2 class="h4 mb-0 text-center" i18n>
|
||||
Ready to take your <strong>investments</strong> to the
|
||||
<strong>next level</strong>?
|
||||
</h2>
|
||||
@ -217,7 +217,7 @@
|
||||
Ghostfolio.
|
||||
</p>
|
||||
<div class="text-center">
|
||||
<a color="primary" href="https://ghostfol.io" mat-flat-button>
|
||||
<a color="primary" href="https://ghostfol.io" i18n mat-flat-button>
|
||||
Get Started
|
||||
</a>
|
||||
</div>
|
||||
|
@ -35,18 +35,18 @@ export const products: Product[] = [
|
||||
isOpenSource: true,
|
||||
key: 'ghostfolio',
|
||||
languages: [
|
||||
'Dutch',
|
||||
'Deutsch',
|
||||
'English',
|
||||
'French',
|
||||
'German',
|
||||
'Italian',
|
||||
'Portuguese',
|
||||
'Spanish'
|
||||
'Español',
|
||||
'Français',
|
||||
'Italiano',
|
||||
'Nederlands',
|
||||
'Português'
|
||||
],
|
||||
name: 'Ghostfolio',
|
||||
origin: 'Switzerland',
|
||||
origin: $localize`Switzerland`,
|
||||
pricingPerYear: '$19',
|
||||
region: 'Global',
|
||||
region: $localize`Global`,
|
||||
slogan: 'Open Source Wealth Management',
|
||||
useAnonymously: true
|
||||
},
|
||||
@ -57,7 +57,7 @@ export const products: Product[] = [
|
||||
isOpenSource: false,
|
||||
key: 'altoo',
|
||||
name: 'Altoo Wealth Platform',
|
||||
origin: 'Switzerland',
|
||||
origin: $localize`Switzerland`,
|
||||
slogan: 'Simplicity for Complex Wealth'
|
||||
},
|
||||
{
|
||||
@ -68,7 +68,7 @@ export const products: Product[] = [
|
||||
isOpenSource: false,
|
||||
key: 'copilot-money',
|
||||
name: 'Copilot Money',
|
||||
origin: 'United States',
|
||||
origin: $localize`United States`,
|
||||
pricingPerYear: '$70',
|
||||
slogan: 'Do money better with Copilot'
|
||||
},
|
||||
@ -81,7 +81,7 @@ export const products: Product[] = [
|
||||
key: 'delta',
|
||||
name: 'Delta Investment Tracker',
|
||||
note: 'Acquired by eToro',
|
||||
origin: 'Belgium',
|
||||
origin: $localize`Belgium`,
|
||||
slogan: 'The app to track all your investments. Make smart moves only.'
|
||||
},
|
||||
{
|
||||
@ -91,9 +91,9 @@ export const products: Product[] = [
|
||||
hasSelfHostingAbility: false,
|
||||
isOpenSource: false,
|
||||
key: 'divvydiary',
|
||||
languages: ['English', 'German'],
|
||||
languages: ['Deutsch', 'English'],
|
||||
name: 'DivvyDiary',
|
||||
origin: 'Germany',
|
||||
origin: $localize`Germany`,
|
||||
pricingPerYear: '€65',
|
||||
slogan: 'Your personal Dividend Calendar'
|
||||
},
|
||||
@ -105,7 +105,7 @@ export const products: Product[] = [
|
||||
isOpenSource: false,
|
||||
key: 'exirio',
|
||||
name: 'Exirio',
|
||||
origin: 'United States',
|
||||
origin: $localize`United States`,
|
||||
pricingPerYear: '$100',
|
||||
slogan: 'All your wealth, in one place.'
|
||||
},
|
||||
@ -115,9 +115,9 @@ export const products: Product[] = [
|
||||
hasSelfHostingAbility: false,
|
||||
isOpenSource: false,
|
||||
key: 'folishare',
|
||||
languages: ['English', 'German'],
|
||||
languages: ['Deutsch', 'English'],
|
||||
name: 'folishare',
|
||||
origin: 'Austria',
|
||||
origin: $localize`Austria`,
|
||||
pricingPerYear: '$65',
|
||||
slogan: 'Take control over your investments'
|
||||
},
|
||||
@ -128,9 +128,9 @@ export const products: Product[] = [
|
||||
hasSelfHostingAbility: false,
|
||||
isOpenSource: false,
|
||||
key: 'getquin',
|
||||
languages: ['English', 'German'],
|
||||
languages: ['Deutsch', 'English'],
|
||||
name: 'getquin',
|
||||
origin: 'Germany',
|
||||
origin: $localize`Germany`,
|
||||
pricingPerYear: '€48',
|
||||
slogan: 'Portfolio Tracker, Analysis & Community'
|
||||
},
|
||||
@ -141,7 +141,7 @@ export const products: Product[] = [
|
||||
isOpenSource: false,
|
||||
key: 'gospatz',
|
||||
name: 'goSPATZ',
|
||||
origin: 'Germany',
|
||||
origin: $localize`Germany`,
|
||||
slogan: 'Volle Kontrolle über deine Investitionen'
|
||||
},
|
||||
{
|
||||
@ -152,7 +152,7 @@ export const products: Product[] = [
|
||||
isOpenSource: false,
|
||||
key: 'justetf',
|
||||
name: 'justETF',
|
||||
origin: 'Germany',
|
||||
origin: $localize`Germany`,
|
||||
pricingPerYear: '€119',
|
||||
slogan: 'ETF portfolios made simple'
|
||||
},
|
||||
@ -164,7 +164,7 @@ export const products: Product[] = [
|
||||
isOpenSource: false,
|
||||
key: 'kubera',
|
||||
name: 'Kubera®',
|
||||
origin: 'United States',
|
||||
origin: $localize`United States`,
|
||||
pricingPerYear: '$150',
|
||||
slogan: 'The Time Machine for your Net Worth'
|
||||
},
|
||||
@ -177,9 +177,9 @@ export const products: Product[] = [
|
||||
key: 'markets.sh',
|
||||
languages: ['English'],
|
||||
name: 'markets.sh',
|
||||
origin: 'Germany',
|
||||
origin: $localize`Germany`,
|
||||
pricingPerYear: '€168',
|
||||
region: 'Global',
|
||||
region: $localize`Global`,
|
||||
slogan: 'Track your investments'
|
||||
},
|
||||
{
|
||||
@ -191,9 +191,9 @@ export const products: Product[] = [
|
||||
languages: ['English'],
|
||||
name: 'Maybe Finance',
|
||||
note: 'Sunset in 2023',
|
||||
origin: 'United States',
|
||||
origin: $localize`United States`,
|
||||
pricingPerYear: '$145',
|
||||
region: 'United States',
|
||||
region: $localize`United States`,
|
||||
slogan: 'Your financial future, in your control'
|
||||
},
|
||||
{
|
||||
@ -215,7 +215,7 @@ export const products: Product[] = [
|
||||
key: 'parqet',
|
||||
name: 'Parqet',
|
||||
note: 'Originally named as Tresor One',
|
||||
origin: 'Germany',
|
||||
origin: $localize`Germany`,
|
||||
pricingPerYear: '€88',
|
||||
region: 'Austria, Germany, Switzerland',
|
||||
slogan: 'Dein Vermögen immer im Blick'
|
||||
@ -227,7 +227,7 @@ export const products: Product[] = [
|
||||
isOpenSource: false,
|
||||
key: 'plannix',
|
||||
name: 'Plannix',
|
||||
origin: 'Italy',
|
||||
origin: $localize`Italy`,
|
||||
slogan: 'Your Personal Finance Hub'
|
||||
},
|
||||
{
|
||||
@ -236,9 +236,9 @@ export const products: Product[] = [
|
||||
hasSelfHostingAbility: false,
|
||||
isOpenSource: false,
|
||||
key: 'portfolio-dividend-tracker',
|
||||
languages: ['English', 'Dutch'],
|
||||
languages: ['English', 'Nederlands'],
|
||||
name: 'Portfolio Dividend Tracker',
|
||||
origin: 'Netherlands',
|
||||
origin: $localize`Netherlands`,
|
||||
pricingPerYear: '€60',
|
||||
slogan: 'Manage all your portfolios'
|
||||
},
|
||||
@ -249,9 +249,9 @@ export const products: Product[] = [
|
||||
hasSelfHostingAbility: false,
|
||||
isOpenSource: false,
|
||||
key: 'portseido',
|
||||
languages: ['Dutch', 'English', 'French', 'German'],
|
||||
languages: ['Deutsch', 'English', 'Français', 'Nederlands'],
|
||||
name: 'Portseido',
|
||||
origin: 'Thailand',
|
||||
origin: $localize`Thailand`,
|
||||
pricingPerYear: '$96',
|
||||
slogan: 'Portfolio Performance and Dividend Tracker'
|
||||
},
|
||||
@ -263,7 +263,7 @@ export const products: Product[] = [
|
||||
isOpenSource: false,
|
||||
key: 'projectionlab',
|
||||
name: 'ProjectionLab',
|
||||
origin: 'United States',
|
||||
origin: $localize`United States`,
|
||||
pricingPerYear: '$108',
|
||||
slogan: 'Build Financial Plans You Love.'
|
||||
},
|
||||
@ -275,7 +275,7 @@ export const products: Product[] = [
|
||||
isOpenSource: false,
|
||||
key: 'seeking-alpha',
|
||||
name: 'Seeking Alpha',
|
||||
origin: 'United States',
|
||||
origin: $localize`United States`,
|
||||
pricingPerYear: '$239',
|
||||
slogan: 'Stock Market Analysis & Tools for Investors'
|
||||
},
|
||||
@ -287,9 +287,9 @@ export const products: Product[] = [
|
||||
isOpenSource: false,
|
||||
key: 'sharesight',
|
||||
name: 'Sharesight',
|
||||
origin: 'New Zealand',
|
||||
origin: $localize`New Zealand`,
|
||||
pricingPerYear: '$135',
|
||||
region: 'Global',
|
||||
region: $localize`Global`,
|
||||
slogan: 'Stock Portfolio Tracker'
|
||||
},
|
||||
{
|
||||
@ -299,7 +299,7 @@ export const products: Product[] = [
|
||||
isOpenSource: false,
|
||||
key: 'simple-portfolio',
|
||||
name: 'Simple Portfolio',
|
||||
origin: 'Czech Republic',
|
||||
origin: $localize`Czech Republic`,
|
||||
pricingPerYear: '€80',
|
||||
slogan: 'Stock Portfolio Tracker'
|
||||
},
|
||||
@ -322,7 +322,7 @@ export const products: Product[] = [
|
||||
isOpenSource: false,
|
||||
key: 'sumio',
|
||||
name: 'Sumio',
|
||||
origin: 'Czech Republic',
|
||||
origin: $localize`Czech Republic`,
|
||||
pricingPerYear: '$20',
|
||||
slogan: 'Sum up and build your wealth.'
|
||||
},
|
||||
@ -332,9 +332,9 @@ export const products: Product[] = [
|
||||
hasSelfHostingAbility: false,
|
||||
isOpenSource: false,
|
||||
key: 'utluna',
|
||||
languages: ['English', 'French', 'German'],
|
||||
languages: ['Deutsch', 'English', 'Français'],
|
||||
name: 'Utluna',
|
||||
origin: 'Switzerland',
|
||||
origin: $localize`Switzerland`,
|
||||
pricingPerYear: '$300',
|
||||
slogan: 'Your Portfolio. Revealed.',
|
||||
useAnonymously: true
|
||||
@ -346,8 +346,8 @@ export const products: Product[] = [
|
||||
isOpenSource: false,
|
||||
key: 'yeekatee',
|
||||
name: 'yeekatee',
|
||||
origin: 'Switzerland',
|
||||
region: 'Switzerland',
|
||||
origin: $localize`Switzerland`,
|
||||
region: $localize`Switzerland`,
|
||||
slogan: 'Connect. Share. Invest.'
|
||||
}
|
||||
];
|
||||
|
@ -8,10 +8,7 @@
|
||||
<div class="col">
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-content>
|
||||
<div
|
||||
*ngIf="hasPermissionToUpdateUserSettings && user?.subscription"
|
||||
class="d-flex py-1"
|
||||
>
|
||||
<div *ngIf="user?.subscription" class="d-flex py-1">
|
||||
<div class="pr-1 w-50" i18n>Membership</div>
|
||||
<div class="pl-1 w-50">
|
||||
<div class="align-items-center d-flex mb-1">
|
||||
@ -28,7 +25,9 @@
|
||||
user?.subscription?.expiresAt | date: defaultDateFormat }}
|
||||
</div>
|
||||
<div *ngIf="user?.subscription?.type === 'Basic'">
|
||||
<ng-container *ngIf="hasPermissionForSubscription">
|
||||
<ng-container
|
||||
*ngIf="hasPermissionForSubscription && hasPermissionToUpdateUserSettings"
|
||||
>
|
||||
<button
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
@ -69,6 +68,7 @@
|
||||
></gf-premium-indicator
|
||||
></a>
|
||||
<a
|
||||
*ngIf="hasPermissionToUpdateUserSettings"
|
||||
class="mr-2 my-2"
|
||||
i18n
|
||||
mat-stroked-button
|
||||
@ -287,24 +287,19 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h2 class="h3 mb-3 text-center" i18n>Granted Access</h2>
|
||||
<h2 class="align-items-center d-flex h3 justify-content-center mb-3">
|
||||
<span i18n>Granted Access</span>
|
||||
<gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator>
|
||||
</h2>
|
||||
<gf-access-table
|
||||
[accesses]="accesses"
|
||||
[hasPermissionToCreateAccess]="hasPermissionToCreateAccess"
|
||||
[showActions]="hasPermissionToDeleteAccess"
|
||||
(accessDeleted)="onDeleteAccess($event)"
|
||||
></gf-access-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="hasPermissionToCreateAccess" class="fab-container">
|
||||
<a
|
||||
class="align-items-center d-flex justify-content-center"
|
||||
color="primary"
|
||||
mat-fab
|
||||
[queryParams]="{ createDialog: true }"
|
||||
[routerLink]="[]"
|
||||
>
|
||||
<ion-icon name="add-outline" size="large"></ion-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user