Compare commits
23 Commits
Author | SHA1 | Date | |
---|---|---|---|
1851ae137f | |||
6f6ff94979 | |||
7f25066f0f | |||
fc795aaa8c | |||
d0112968e8 | |||
522025ffa0 | |||
27bf662281 | |||
93c27277c6 | |||
5e6adfcef5 | |||
ab691bb27a | |||
8fc5676443 | |||
1fe1e2fe0c | |||
921d38a706 | |||
6161d5e77c | |||
369386f976 | |||
41437636b1 | |||
b21884eb66 | |||
1c5437e1fd | |||
58278ba5e6 | |||
921f3e9807 | |||
75ca125a70 | |||
a1fd4e7a38 | |||
0d5a8eb33e |
@ -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>
|
||||
|
38
CHANGELOG.md
38
CHANGELOG.md
@ -5,6 +5,40 @@ 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.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
|
||||
@ -1509,7 +1543,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
|
||||
@ -2932,7 +2966,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;
|
||||
|
||||
|
@ -8,7 +8,9 @@ import { PropertyService } from '@ghostfolio/api/services/property/property.serv
|
||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
||||
import {
|
||||
DEFAULT_CURRENCY,
|
||||
PROPERTY_CURRENCIES
|
||||
PROPERTY_CURRENCIES,
|
||||
PROPERTY_IS_READ_ONLY_MODE,
|
||||
PROPERTY_IS_USER_SIGNUP_ENABLED
|
||||
} from '@ghostfolio/common/config';
|
||||
import {
|
||||
AdminData,
|
||||
@ -305,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();
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -8,6 +8,7 @@ import { PropertyService } from '@ghostfolio/api/services/property/property.serv
|
||||
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,
|
||||
@ -168,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;
|
||||
}
|
||||
@ -185,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);
|
||||
|
||||
@ -195,7 +213,7 @@ export class InfoService {
|
||||
).text()
|
||||
);
|
||||
} catch (error) {
|
||||
Logger.error(error, 'InfoService');
|
||||
Logger.error(error, 'InfoService - GitHub');
|
||||
|
||||
return undefined;
|
||||
}
|
||||
@ -203,16 +221,24 @@ 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;
|
||||
}
|
||||
@ -323,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();
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -19,7 +19,7 @@ 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');
|
||||
|
||||
@ -188,6 +188,11 @@ export class UserService {
|
||||
currentPermissions.push(permissions.enableSubscriptionInterstitial);
|
||||
}
|
||||
|
||||
currentPermissions = without(
|
||||
currentPermissions,
|
||||
permissions.createAccess
|
||||
);
|
||||
|
||||
// Reset benchmark
|
||||
user.Settings.settings.benchmark = undefined;
|
||||
}
|
||||
|
@ -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>
|
||||
@ -550,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>
|
||||
@ -559,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>
|
||||
@ -601,7 +809,7 @@
|
||||
<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>
|
||||
|
@ -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']
|
||||
|
@ -4,7 +4,10 @@ import {
|
||||
IDataProviderHistoricalResponse,
|
||||
IDataProviderResponse
|
||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
|
||||
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';
|
||||
@ -40,7 +43,16 @@ export class CoinGeckoService implements DataProviderInterface {
|
||||
};
|
||||
|
||||
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) {
|
||||
@ -73,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=${DEFAULT_CURRENCY.toLowerCase()}&from=${getUnixTime(
|
||||
from
|
||||
)}&to=${getUnixTime(to)}`
|
||||
)}&to=${getUnixTime(to)}`,
|
||||
{
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
).json<any>();
|
||||
|
||||
const result: {
|
||||
@ -122,10 +144,20 @@ 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=${DEFAULT_CURRENCY.toLowerCase()}`
|
||||
)}&vs_currencies=${DEFAULT_CURRENCY.toLowerCase()}`,
|
||||
{
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
}
|
||||
).json<any>();
|
||||
|
||||
for (const symbol in response) {
|
||||
@ -160,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 {
|
||||
|
@ -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(() => {
|
||||
|
@ -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>();
|
||||
|
||||
@ -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,7 +5,10 @@ import {
|
||||
IDataProviderHistoricalResponse,
|
||||
IDataProviderResponse
|
||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
|
||||
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';
|
||||
@ -63,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: {
|
||||
@ -110,8 +123,18 @@ 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) {
|
||||
@ -144,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,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"
|
||||
@ -246,18 +249,22 @@
|
||||
</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"
|
||||
|
@ -7,6 +7,16 @@
|
||||
.mat-toolbar {
|
||||
background-color: var(--light-background);
|
||||
|
||||
.logo-container {
|
||||
&.filled {
|
||||
background-color: rgba(var(--palette-foreground-base), 0.02);
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
width: 14rem;
|
||||
}
|
||||
}
|
||||
|
||||
.list-inline-item {
|
||||
margin: 0;
|
||||
}
|
||||
@ -34,5 +44,11 @@
|
||||
: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;
|
||||
|
@ -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) {
|
||||
|
@ -34,7 +34,7 @@
|
||||
and the path we are on.
|
||||
</p>
|
||||
<p>
|
||||
Now, we’re thrilled to present Ghostfolio 2.0, another milestone in
|
||||
Now, we are thrilled to present Ghostfolio 2.0, another milestone in
|
||||
our journey to empower investors.
|
||||
</p>
|
||||
</section>
|
||||
@ -44,10 +44,10 @@
|
||||
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 increased
|
||||
stability, extended data import capabilities, added comprehensive
|
||||
analytics and utilized the latest technology to deliver these
|
||||
improvements. Here’s a closer look at a selection of the
|
||||
>, 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.
|
||||
@ -79,7 +79,7 @@
|
||||
<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’ve upgraded to
|
||||
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
|
||||
@ -94,15 +94,20 @@
|
||||
<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 wouldn’t have
|
||||
been possible. As we celebrate the launch of Ghostfolio 2.0, we’re
|
||||
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 on GitHub</strong>,
|
||||
highlighting the appreciation and adoption of our platform by the
|
||||
community.
|
||||
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
|
||||
@ -119,9 +124,10 @@
|
||||
<li>
|
||||
On
|
||||
<a href="https://twitter.com/ghostfolio_" target="_blank">X</a>
|
||||
(formerly Twitter), over <strong>300 investors</strong> and
|
||||
enthusiasts follow Ghostfolio, keen to stay updated on the latest
|
||||
developments
|
||||
(formerly Twitter), over
|
||||
<strong>300 investors and personal finance enthusiasts</strong>
|
||||
follow Ghostfolio, keen to stay updated on the latest
|
||||
developments.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
@ -139,7 +145,7 @@
|
||||
title="Join the Ghostfolio Slack community"
|
||||
>Slack</a
|
||||
>
|
||||
community. It’s a dynamic space where you can learn, collaborate,
|
||||
community. It is a dynamic space where you can learn, collaborate,
|
||||
and grow together with us.
|
||||
</p>
|
||||
<p>
|
||||
@ -147,13 +153,13 @@
|
||||
insights, follow
|
||||
<a href="https://twitter.com/ghostfolio_" target="_blank"
|
||||
>Ghostfolio on X</a
|
||||
>. It’s the perfect place to stay informed and connect with our
|
||||
>. It is the perfect place to stay informed and connect with our
|
||||
team.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Give us a Star</strong>: If you’ve found value in Ghostfolio
|
||||
or appreciate our commitment to simplifying investment tracking,
|
||||
please consider giving us a star on
|
||||
<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"
|
||||
@ -163,7 +169,7 @@
|
||||
difference in the world of wealth management.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Become a contributor</strong>: If you’re a developer
|
||||
<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"
|
||||
@ -175,7 +181,7 @@
|
||||
<section>
|
||||
<p>
|
||||
Ghostfolio 2.0 represents a major step forward in our mission to
|
||||
empower investors, and we couldn’t be more excited about the future
|
||||
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.
|
||||
|
@ -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,7 +1,7 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col text-center">
|
||||
<div class="mt-5">
|
||||
<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>
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -333,7 +333,7 @@
|
||||
>
|
||||
</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()">
|
||||
|
@ -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>
|
||||
|
@ -6,13 +6,6 @@
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.fab-container {
|
||||
position: fixed;
|
||||
right: 2rem;
|
||||
bottom: 2rem;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.hint-text {
|
||||
font-size: 90%;
|
||||
line-height: 1.2;
|
||||
|
@ -1,33 +1,27 @@
|
||||
import { ViewportScroller } from '@angular/common';
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { TabConfiguration, User } from '@ghostfolio/common/interfaces';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject } from 'rxjs';
|
||||
import { first, takeUntil } from 'rxjs/operators';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page has-tabs' },
|
||||
selector: 'gf-zen-page',
|
||||
templateUrl: './zen-page.html',
|
||||
styleUrls: ['./zen-page.scss']
|
||||
styleUrls: ['./zen-page.scss'],
|
||||
templateUrl: './zen-page.html'
|
||||
})
|
||||
export class ZenPageComponent implements AfterViewInit, OnDestroy, OnInit {
|
||||
export class ZenPageComponent implements OnDestroy, OnInit {
|
||||
public deviceType: string;
|
||||
public tabs: TabConfiguration[] = [];
|
||||
public user: User;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor(
|
||||
private route: ActivatedRoute,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private userService: UserService,
|
||||
private viewportScroller: ViewportScroller
|
||||
private deviceService: DeviceDetectorService,
|
||||
private userService: UserService
|
||||
) {
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
@ -52,12 +46,8 @@ export class ZenPageComponent implements AfterViewInit, OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnInit() {}
|
||||
|
||||
public ngAfterViewInit(): void {
|
||||
this.route.fragment
|
||||
.pipe(first())
|
||||
.subscribe((fragment) => this.viewportScroller.scrollToAnchor(fragment));
|
||||
public ngOnInit() {
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
|
@ -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) {
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -274,6 +274,22 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.page {
|
||||
&.has-tabs {
|
||||
.mat-mdc-tab-nav-bar {
|
||||
--mat-tab-header-inactive-label-text-color: rgba(
|
||||
var(--light-primary-text)
|
||||
);
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.mat-mdc-tab-header {
|
||||
background-color: rgba(var(--palette-foreground-base-dark), 0.02);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.svgMap-tooltip {
|
||||
background: var(--dark-background);
|
||||
|
||||
@ -357,6 +373,12 @@ ngx-skeleton-loader {
|
||||
@include gf-table;
|
||||
}
|
||||
|
||||
.has-info-message {
|
||||
.page.has-tabs {
|
||||
height: calc(100vh - 2 * var(--mat-toolbar-standard-height));
|
||||
}
|
||||
}
|
||||
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
@ -430,6 +452,11 @@ ngx-skeleton-loader {
|
||||
}
|
||||
}
|
||||
|
||||
.mat-stepper-vertical,
|
||||
.mat-stepper-horizontal {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.mdc-button {
|
||||
&.mat-accent,
|
||||
&.mat-primary {
|
||||
@ -450,7 +477,54 @@ ngx-skeleton-loader {
|
||||
}
|
||||
|
||||
.page {
|
||||
padding-bottom: 5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
|
||||
&:not(.has-tabs) {
|
||||
@media (min-width: 576px) {
|
||||
padding: 2rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.has-tabs {
|
||||
height: calc(100vh - var(--mat-toolbar-standard-height));
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
|
||||
.mat-mdc-tab-nav-bar {
|
||||
--mat-tab-header-active-focus-indicator-color: transparent;
|
||||
--mat-tab-header-active-hover-indicator-color: transparent;
|
||||
--mat-tab-header-inactive-label-text-color: rgba(
|
||||
var(--dark-primary-text)
|
||||
);
|
||||
--mdc-tab-indicator-active-indicator-color: transparent;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.mat-mdc-tab-header {
|
||||
background-color: rgba(var(--palette-foreground-base), 0.02);
|
||||
padding: 2rem 0;
|
||||
width: 14rem;
|
||||
--mat-tab-header-label-text-tracking: normal;
|
||||
--mdc-secondary-navigation-tab-container-height: 2rem;
|
||||
|
||||
.mat-mdc-tab-links {
|
||||
flex-direction: column;
|
||||
|
||||
.mat-mdc-tab-link {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mat-mdc-tab-nav-panel {
|
||||
padding: 2rem 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.svgMap-tooltip {
|
||||
@ -465,10 +539,6 @@ ngx-skeleton-loader {
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
||||
.with-info-message {
|
||||
height: calc(100vh - 5rem - 3.5rem + 0.5rem) !important;
|
||||
}
|
||||
|
||||
.with-placeholder-as-option {
|
||||
.mat-mdc-select-placeholder {
|
||||
color: rgba(var(--dark-primary-text));
|
||||
|
@ -17,7 +17,7 @@ services:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
postgres:
|
||||
image: postgres:12
|
||||
image: postgres:15
|
||||
env_file:
|
||||
- ../.env
|
||||
healthcheck:
|
||||
|
@ -1,7 +1,7 @@
|
||||
version: '3.9'
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:12
|
||||
image: postgres:15
|
||||
container_name: postgres
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
|
@ -17,7 +17,7 @@ services:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
postgres:
|
||||
image: postgres:12
|
||||
image: postgres:15
|
||||
env_file:
|
||||
- ../.env
|
||||
healthcheck:
|
||||
|
@ -39,7 +39,7 @@ export const DEFAULT_CURRENCY = 'USD';
|
||||
export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy';
|
||||
export const DEFAULT_LANGUAGE_CODE = 'en';
|
||||
export const DEFAULT_PAGE_SIZE = 50;
|
||||
export const DEFAULT_REQUEST_TIMEOUT = ms('3 seconds');
|
||||
export const DEFAULT_REQUEST_TIMEOUT = ms('2 seconds');
|
||||
export const DEFAULT_ROOT_URL = 'http://localhost:4200';
|
||||
|
||||
export const EMERGENCY_FUND_TAG_ID = '4452656d-9fa4-4bd0-ba38-70492e31d180';
|
||||
|
@ -5,7 +5,10 @@ export interface Export {
|
||||
date: string;
|
||||
version: string;
|
||||
};
|
||||
accounts: Omit<Account, 'createdAt' | 'isDefault' | 'updatedAt' | 'userId'>[];
|
||||
accounts: Omit<
|
||||
Account,
|
||||
'accountType' | 'createdAt' | 'isDefault' | 'updatedAt' | 'userId'
|
||||
>[];
|
||||
activities: (Omit<
|
||||
Order,
|
||||
| 'accountUserId'
|
||||
|
@ -21,7 +21,6 @@ const locales = {
|
||||
PRESET_ID: $localize`Preset`,
|
||||
RETIREMENT_PROVISION: $localize`Retirement Provision`,
|
||||
SATELLITE: $localize`Satellite`,
|
||||
SECURITIES: $localize`Securities`,
|
||||
SYMBOL: $localize`Symbol`,
|
||||
TAG: $localize`Tag`,
|
||||
YEAR: $localize`Year`,
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ghostfolio",
|
||||
"version": "2.0.0",
|
||||
"version": "2.2.0",
|
||||
"homepage": "https://ghostfol.io",
|
||||
"license": "AGPL-3.0",
|
||||
"repository": "https://github.com/ghostfolio/ghostfolio",
|
||||
|
@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Account" ALTER COLUMN "accountType" DROP NOT NULL,
|
||||
ALTER COLUMN "accountType" DROP DEFAULT;
|
@ -21,7 +21,7 @@ model Access {
|
||||
}
|
||||
|
||||
model Account {
|
||||
accountType AccountType @default(SECURITIES)
|
||||
accountType AccountType?
|
||||
balance Float @default(0)
|
||||
balances AccountBalance[]
|
||||
comment String?
|
||||
|
@ -5,7 +5,6 @@
|
||||
},
|
||||
"accounts": [
|
||||
{
|
||||
"accountType": "SECURITIES",
|
||||
"balance": 2000,
|
||||
"currency": "USD",
|
||||
"id": "b2d3fe1d-d6a8-41a3-be39-07ef5e9480f0",
|
||||
@ -6016,4 +6015,4 @@
|
||||
"symbol": "AAPL"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,6 @@
|
||||
},
|
||||
"accounts": [
|
||||
{
|
||||
"accountType": "SECURITIES",
|
||||
"balance": 2000,
|
||||
"currency": "USD",
|
||||
"id": "b2d3fe1d-d6a8-41a3-be39-07ef5e9480f0",
|
||||
|
Reference in New Issue
Block a user