Compare commits

..

29 Commits

Author SHA1 Message Date
55b03733f4 Release 1.231.0 (#1654) 2023-02-04 11:28:48 +01:00
0000317041 Upgrade Nx and Angular (#1646)
* Upgrade Nx and Angular

* Update changelog

* Feature/eliminate angular material css vars (#1648)

* Eliminate angular-material-css-vars

* Update changelog
2023-02-04 11:26:06 +01:00
e5f2a3865d Update types of node and papaparse (#1650) 2023-02-03 13:27:59 +01:00
c61561664f Relax validation for REDIS_HOST (#1652)
* Relax validation for REDIS_HOST

* Update changelog
2023-02-02 13:39:13 +01:00
a7d8a63ab8 Remove year (#1649) 2023-02-01 08:58:17 +01:00
5c51c1e825 Reorder features (#1644) 2023-01-30 20:22:03 +01:00
3a67bf9bb4 Add /de/pricing and /de/blog (#1645) 2023-01-30 20:21:37 +01:00
f7597c213d Feature/add dividend and fees to position detail dialog (#1643)
* Add dividend and fees to position detail dialog

* Update changelog
2023-01-30 20:21:16 +01:00
2e7f46ad78 Feature/allow account for activity of type item (#1641)
* Support linking wealth items to account

* Update changelog
2023-01-30 20:00:07 +01:00
cfffb99f52 Feature/extract locales 20230129 (#1642)
* Improve translations

* Update changelog
2023-01-30 19:45:27 +01:00
69ac3408f1 Release 1.230.0 (#1639) 2023-01-29 10:00:28 +01:00
e1806b4bd8 Feature/add sourceforge logo to landing page (#1638)
* Add SourceForge

* Update changelog
2023-01-29 09:58:53 +01:00
6aae0cc1e4 Bugfix/fix issue with value in value redaction interceptor (#1627)
* Fix issue with value in value redaction interceptor

* Fix format of world map

* Update changelog
2023-01-29 09:58:05 +01:00
5d8a50a80d Feature/add interstitial for subscription (#1637)
* Add interstitial

* Improve pricing page

* Update changelog
2023-01-28 09:42:15 +01:00
662231e830 Feature/upgrade prisma to version 4.9.0 (#1630)
* Upgrade prisma to version 4.9.0

* Update changelog
2023-01-28 09:41:33 +01:00
4d84459b5b Clean up imports (#1632) 2023-01-27 08:49:31 +01:00
efba7429c1 Bugfix/fix click of unknown accounts (#1629)
* Check for unknown key

* Update changelog
2023-01-25 12:00:52 +01:00
9cae5a3e79 Feature/improve sackgeld.com blog post (#1628)
* Improve blog post and add quote

* Update changelog
2023-01-24 08:11:21 +01:00
c2ed0a436f Downgrade to Node.js 16 (for development) (#1633) 2023-01-23 21:52:46 +01:00
8486c02575 Update Node to version 18.x (#1595)
* Update Node to version 18.x

* Add .nvmrc

* Update changelog

Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2023-01-23 08:28:19 +01:00
5122ef3456 Release 1.229.0 (#1626) 2023-01-21 16:10:04 +01:00
579b86665e Feature/add sackgeld.com blog post (#1625)
* Add blog post: Ghostfolio auf Sackgeld.com vorgestellt

* Update changelog
2023-01-21 16:07:32 +01:00
52b3ad6dc3 Feature/refactor value redaction interceptor (#1624)
* Reuse redactAttributes()

* Update changelog
2023-01-21 11:46:56 +01:00
bf9b60aa74 Feature/add sackgeld.com to as seen in section (#1622)
* Add Sackgeld.com

* Update changelog
2023-01-21 11:31:52 +01:00
6cd51fb044 Feature/hide irrelevant errors in client (#1623)
* Hide irrelevant errors in client

* Update changelog
2023-01-21 11:30:53 +01:00
271001f523 Feature/remove toggle on allocations page (#1620)
* Rename allocationCurrent, remove allocationInvestment

* Update changelog
2023-01-21 09:52:58 +01:00
a7e513a6d1 Bugfix/fix filtered value for emergency fund (#1619)
* Fix filtered value

* Update changelog
2023-01-20 20:51:08 +01:00
b5f256be95 Remove mail address (#1621) 2023-01-20 20:50:32 +01:00
a834ef6b4c Release 1.228.1 (#1617) 2023-01-18 22:01:59 +01:00
166 changed files with 7118 additions and 2948 deletions

View File

@ -10,7 +10,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node_version: node_version:
- 16 - 18
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v3

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
v16

View File

@ -5,7 +5,59 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 1.228.0 - 2023-01-18 ## 1.231.0 - 2023-02-04
### Added
- Added the dividend and fees to the position detail dialog
- Added support to link a (wealth) item to an account
### Changed
- Relaxed the validation rule of the _Redis_ host environment variable (`REDIS_HOST`)
- Improved the language localization for German (`de`)
- Eliminated `angular-material-css-vars`
- Upgraded `angular` from version `14.2.0` to `15.1.2`
- Upgraded `Nx` from version `15.0.13` to `15.6.3`
## 1.230.0 - 2023-01-29
### Added
- Added an interstitial for the subscription
- Added _SourceForge_ to the _As seen in_ section on the landing page
- Added a quote to the blog post _Ghostfolio auf Sackgeld.com vorgestellt_
### Changed
- Improved the unit format (`%`) in the global heat map component of the public page
- Improved the pricing page
- Upgraded `Node.js` from version `16` to `18` (`Dockerfile`)
- Upgraded `prisma` from version `4.8.0` to `4.9.0`
### Fixed
- Fixed the click of unknown accounts in the portfolio proportion chart component
- Fixed an issue with `value` in the value redaction interceptor for the impersonation mode
## 1.229.0 - 2023-01-21
### Added
- Added a blog post: _Ghostfolio auf Sackgeld.com vorgestellt_
- Added _Sackgeld.com_ to the _As seen in_ section on the landing page
### Changed
- Removed the toggle _Original Shares_ vs. _Current Shares_ on the allocations page
- Hid error messages related to no current investment in the client
- Refactored the value redaction interceptor for the impersonation mode
### Fixed
- Fixed the value of the active (emergency fund) filter in percentage on the allocations page
## 1.228.1 - 2023-01-18
### Added ### Added

View File

@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM node:16-slim as builder FROM --platform=$BUILDPLATFORM node:18-slim as builder
# Build application and add additional files # Build application and add additional files
WORKDIR /ghostfolio WORKDIR /ghostfolio
@ -50,7 +50,7 @@ COPY package.json /ghostfolio/dist/apps/api
RUN yarn database:generate-typings RUN yarn database:generate-typings
# Image to run, copy everything needed from builder # Image to run, copy everything needed from builder
FROM node:16-slim FROM node:18-slim
RUN apt update && apt install -y \ RUN apt update && apt install -y \
openssl \ openssl \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*

View File

@ -40,7 +40,7 @@ Ghostfolio is for you if you are...
- 🧘 into minimalism - 🧘 into minimalism
- 🧺 caring about diversifying your financial resources - 🧺 caring about diversifying your financial resources
- 🆓 interested in financial independence - 🆓 interested in financial independence
- 🙅 saying no to spreadsheets in 2023 - 🙅 saying no to spreadsheets
- 😎 still reading this list - 😎 still reading this list
## Features ## Features
@ -148,7 +148,7 @@ Please follow the instructions of the Ghostfolio [Unraid Community App](https://
### Prerequisites ### Prerequisites
- [Docker](https://www.docker.com/products/docker-desktop) - [Docker](https://www.docker.com/products/docker-desktop)
- [Node.js](https://nodejs.org/en/download) (version 16+) - [Node.js](https://nodejs.org/en/download) (version 16)
- [Yarn](https://yarnpkg.com/en/docs/install) - [Yarn](https://yarnpkg.com/en/docs/install)
- A local copy of this Git repository (clone) - A local copy of this Git repository (clone)
@ -269,7 +269,7 @@ You can get the _Bearer Token_ via `GET http://localhost:3333/api/v1/auth/anonym
Ghostfolio is **100% free** and **open source**. We encourage and support an active and healthy community that accepts contributions from the public - including you. Ghostfolio is **100% free** and **open source**. We encourage and support an active and healthy community that accepts contributions from the public - including you.
Not sure what to work on? We have got some ideas. Please join the Ghostfolio [Slack channel](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg), tweet to [@ghostfolio\_](https://twitter.com/ghostfolio_) or send an e-mail to hi@ghostfol.io. We would love to hear from you. Not sure what to work on? We have got some ideas. Please join the Ghostfolio [Slack channel](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg) or tweet to [@ghostfolio\_](https://twitter.com/ghostfolio_). We would love to hear from you.
If you like to support this project, get [**Ghostfolio Premium**](https://ghostfol.io/en/pricing) or [**Buy me a coffee**](https://www.buymeacoffee.com/ghostfolio). If you like to support this project, get [**Ghostfolio Premium**](https://ghostfol.io/en/pricing) or [**Buy me a coffee**](https://www.buymeacoffee.com/ghostfolio).

View File

@ -1,9 +1,6 @@
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
import { UserService } from '@ghostfolio/api/app/user/user.service'; import { UserService } from '@ghostfolio/api/app/user/user.service';
import { import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor';
nullifyValuesInObject,
nullifyValuesInObjects
} from '@ghostfolio/api/helper/object.helper';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
import { Accounts } from '@ghostfolio/common/interfaces'; import { Accounts } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
@ -22,7 +19,8 @@ import {
Param, Param,
Post, Post,
Put, Put,
UseGuards UseGuards,
UseInterceptors
} from '@nestjs/common'; } from '@nestjs/common';
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
@ -39,8 +37,7 @@ export class AccountController {
private readonly accountService: AccountService, private readonly accountService: AccountService,
private readonly impersonationService: ImpersonationService, private readonly impersonationService: ImpersonationService,
private readonly portfolioService: PortfolioService, private readonly portfolioService: PortfolioService,
@Inject(REQUEST) private readonly request: RequestWithUser, @Inject(REQUEST) private readonly request: RequestWithUser
private readonly userService: UserService
) {} ) {}
@Delete(':id') @Delete(':id')
@ -85,6 +82,7 @@ export class AccountController {
@Get() @Get()
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
@UseInterceptors(RedactValuesInResponseInterceptor)
public async getAllAccounts( public async getAllAccounts(
@Headers('impersonation-id') impersonationId @Headers('impersonation-id') impersonationId
): Promise<Accounts> { ): Promise<Accounts> {
@ -94,39 +92,15 @@ export class AccountController {
this.request.user.id this.request.user.id
); );
let accountsWithAggregations = return this.portfolioService.getAccountsWithAggregations({
await this.portfolioService.getAccountsWithAggregations({
userId: impersonationUserId || this.request.user.id, userId: impersonationUserId || this.request.user.id,
withExcludedAccounts: true withExcludedAccounts: true
}); });
if (
impersonationUserId ||
this.userService.isRestrictedView(this.request.user)
) {
accountsWithAggregations = {
...nullifyValuesInObject(accountsWithAggregations, [
'totalBalanceInBaseCurrency',
'totalValueInBaseCurrency'
]),
accounts: nullifyValuesInObjects(accountsWithAggregations.accounts, [
'balance',
'balanceInBaseCurrency',
'convertedBalance',
'fee',
'quantity',
'unitPrice',
'value',
'valueInBaseCurrency'
])
};
}
return accountsWithAggregations;
} }
@Get(':id') @Get(':id')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
@UseInterceptors(RedactValuesInResponseInterceptor)
public async getAccountById( public async getAccountById(
@Headers('impersonation-id') impersonationId, @Headers('impersonation-id') impersonationId,
@Param('id') id: string @Param('id') id: string
@ -137,35 +111,13 @@ export class AccountController {
this.request.user.id this.request.user.id
); );
let accountsWithAggregations = const accountsWithAggregations =
await this.portfolioService.getAccountsWithAggregations({ await this.portfolioService.getAccountsWithAggregations({
filters: [{ id, type: 'ACCOUNT' }], filters: [{ id, type: 'ACCOUNT' }],
userId: impersonationUserId || this.request.user.id, userId: impersonationUserId || this.request.user.id,
withExcludedAccounts: true withExcludedAccounts: true
}); });
if (
impersonationUserId ||
this.userService.isRestrictedView(this.request.user)
) {
accountsWithAggregations = {
...nullifyValuesInObject(accountsWithAggregations, [
'totalBalanceInBaseCurrency',
'totalValueInBaseCurrency'
]),
accounts: nullifyValuesInObjects(accountsWithAggregations.accounts, [
'balance',
'balanceInBaseCurrency',
'convertedBalance',
'fee',
'quantity',
'unitPrice',
'value',
'valueInBaseCurrency'
])
};
}
return accountsWithAggregations.accounts[0]; return accountsWithAggregations.accounts[0];
} }

View File

@ -1,24 +1,23 @@
import { join } from 'path'; import { join } from 'path';
import { AuthDeviceModule } from '@ghostfolio/api/app/auth-device/auth-device.module';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
import { CronService } from '@ghostfolio/api/services/cron.service';
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
import { TwitterBotModule } from '@ghostfolio/api/services/twitter-bot/twitter-bot.module';
import { BullModule } from '@nestjs/bull'; import { BullModule } from '@nestjs/bull';
import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common'; import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { ScheduleModule } from '@nestjs/schedule'; import { ScheduleModule } from '@nestjs/schedule';
import { ServeStaticModule } from '@nestjs/serve-static'; import { ServeStaticModule } from '@nestjs/serve-static';
import { ConfigurationModule } from '../services/configuration.module';
import { CronService } from '../services/cron.service';
import { DataGatheringModule } from '../services/data-gathering.module';
import { DataProviderModule } from '../services/data-provider/data-provider.module';
import { ExchangeRateDataModule } from '../services/exchange-rate-data.module';
import { PrismaModule } from '../services/prisma.module';
import { TwitterBotModule } from '../services/twitter-bot/twitter-bot.module';
import { AccessModule } from './access/access.module'; import { AccessModule } from './access/access.module';
import { AccountModule } from './account/account.module'; import { AccountModule } from './account/account.module';
import { AdminModule } from './admin/admin.module'; import { AdminModule } from './admin/admin.module';
import { AppController } from './app.controller'; import { AppController } from './app.controller';
import { AuthDeviceModule } from './auth-device/auth-device.module';
import { AuthModule } from './auth/auth.module'; import { AuthModule } from './auth/auth.module';
import { BenchmarkModule } from './benchmark/benchmark.module'; import { BenchmarkModule } from './benchmark/benchmark.module';
import { CacheModule } from './cache/cache.module'; import { CacheModule } from './cache/cache.module';
@ -30,6 +29,7 @@ import { InfoModule } from './info/info.module';
import { LogoModule } from './logo/logo.module'; import { LogoModule } from './logo/logo.module';
import { OrderModule } from './order/order.module'; import { OrderModule } from './order/order.module';
import { PortfolioModule } from './portfolio/portfolio.module'; import { PortfolioModule } from './portfolio/portfolio.module';
import { RedisCacheModule } from './redis-cache/redis-cache.module';
import { SubscriptionModule } from './subscription/subscription.module'; import { SubscriptionModule } from './subscription/subscription.module';
import { SymbolModule } from './symbol/symbol.module'; import { SymbolModule } from './symbol/symbol.module';
import { UserModule } from './user/user.module'; import { UserModule } from './user/user.module';
@ -45,7 +45,7 @@ import { UserModule } from './user/user.module';
BullModule.forRoot({ BullModule.forRoot({
redis: { redis: {
host: process.env.REDIS_HOST, host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT, 10), port: parseInt(process.env.REDIS_PORT ?? '6379', 10),
password: process.env.REDIS_PASSWORD password: process.env.REDIS_PASSWORD
} }
}), }),

View File

@ -83,6 +83,13 @@ export class FrontendMiddleware implements NestMiddleware {
) { ) {
featureGraphicPath = 'assets/images/blog/20221226.jpg'; featureGraphicPath = 'assets/images/blog/20221226.jpg';
title = `The importance of tracking your personal finances - ${title}`; title = `The importance of tracking your personal finances - ${title}`;
} else if (
request.path.startsWith(
'/de/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt'
)
) {
featureGraphicPath = 'assets/images/blog/ghostfolio-x-sackgeld.png';
title = `Ghostfolio auf Sackgeld.com vorgestellt - ${title}`;
} }
if ( if (

View File

@ -20,7 +20,6 @@ import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { isEmpty } from 'lodash';
import { ImportDataDto } from './import-data.dto'; import { ImportDataDto } from './import-data.dto';
import { ImportService } from './import.service'; import { ImportService } from './import.service';

View File

@ -76,22 +76,20 @@ export class OrderService {
userId: string; userId: string;
} }
): Promise<Order> { ): Promise<Order> {
const defaultAccount = ( let Account;
await this.accountService.getAccounts(data.userId)
).find((account) => {
return account.isDefault === true;
});
const tags = data.tags ?? []; if (data.accountId) {
Account = {
let Account = {
connect: { connect: {
id_userId: { id_userId: {
userId: data.userId, userId: data.userId,
id: data.accountId ?? defaultAccount?.id id: data.accountId
} }
} }
}; };
}
const tags = data.tags ?? [];
if (data.type === 'ITEM') { if (data.type === 'ITEM') {
const assetClass = data.assetClass; const assetClass = data.assetClass;
@ -101,7 +99,6 @@ export class OrderService {
const id = uuidv4(); const id = uuidv4();
const name = data.SymbolProfile.connectOrCreate.create.symbol; const name = data.SymbolProfile.connectOrCreate.create.symbol;
Account = undefined;
data.id = id; data.id = id;
data.SymbolProfile.connectOrCreate.create.assetClass = assetClass; data.SymbolProfile.connectOrCreate.create.assetClass = assetClass;
data.SymbolProfile.connectOrCreate.create.assetSubClass = assetSubClass; data.SymbolProfile.connectOrCreate.create.assetSubClass = assetSubClass;

View File

@ -7,6 +7,8 @@ import { Tag } from '@prisma/client';
export interface PortfolioPositionDetail { export interface PortfolioPositionDetail {
averagePrice: number; averagePrice: number;
dividendInBaseCurrency: number;
feeInBaseCurrency: number;
firstBuyDate: string; firstBuyDate: string;
grossPerformance: number; grossPerformance: number;
grossPerformancePercent: number; grossPerformancePercent: number;

View File

@ -82,6 +82,7 @@ describe('PortfolioCalculator', () => {
averagePrice: new Big('0'), averagePrice: new Big('0'),
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
fee: new Big('3.2'),
firstBuyDate: '2021-11-22', firstBuyDate: '2021-11-22',
grossPerformance: new Big('-12.6'), grossPerformance: new Big('-12.6'),
grossPerformancePercentage: new Big('-0.0440867739678096571'), grossPerformancePercentage: new Big('-0.0440867739678096571'),

View File

@ -71,6 +71,7 @@ describe('PortfolioCalculator', () => {
averagePrice: new Big('136.6'), averagePrice: new Big('136.6'),
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
fee: new Big('1.55'),
firstBuyDate: '2021-11-30', firstBuyDate: '2021-11-30',
grossPerformance: new Big('24.6'), grossPerformance: new Big('24.6'),
grossPerformancePercentage: new Big('0.09004392386530014641'), grossPerformancePercentage: new Big('0.09004392386530014641'),

View File

@ -82,6 +82,7 @@ describe('PortfolioCalculator', () => {
averagePrice: new Big('320.43'), averagePrice: new Big('320.43'),
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
fee: new Big('0'),
firstBuyDate: '2015-01-01', firstBuyDate: '2015-01-01',
grossPerformance: new Big('27172.74'), grossPerformance: new Big('27172.74'),
grossPerformancePercentage: new Big('42.40043067128546016291'), grossPerformancePercentage: new Big('42.40043067128546016291'),

View File

@ -82,6 +82,7 @@ describe('PortfolioCalculator', () => {
averagePrice: new Big('75.80'), averagePrice: new Big('75.80'),
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
fee: new Big('4.25'),
firstBuyDate: '2022-03-07', firstBuyDate: '2022-03-07',
grossPerformance: new Big('21.93'), grossPerformance: new Big('21.93'),
grossPerformancePercentage: new Big('0.14465699208443271768'), grossPerformancePercentage: new Big('0.14465699208443271768'),

View File

@ -102,6 +102,7 @@ describe('PortfolioCalculator', () => {
averagePrice: new Big('0'), averagePrice: new Big('0'),
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
fee: new Big('0'),
firstBuyDate: '2022-03-07', firstBuyDate: '2022-03-07',
grossPerformance: new Big('19.86'), grossPerformance: new Big('19.86'),
grossPerformancePercentage: new Big('0.13100263852242744063'), grossPerformancePercentage: new Big('0.13100263852242744063'),

View File

@ -431,6 +431,7 @@ export class PortfolioCalculator {
: item.investment.div(item.quantity), : item.investment.div(item.quantity),
currency: item.currency, currency: item.currency,
dataSource: item.dataSource, dataSource: item.dataSource,
fee: item.fee,
firstBuyDate: item.firstBuyDate, firstBuyDate: item.firstBuyDate,
grossPerformance: !hasErrors ? grossPerformance ?? null : null, grossPerformance: !hasErrors ? grossPerformance ?? null : null,
grossPerformancePercentage: !hasErrors grossPerformancePercentage: !hasErrors
@ -447,7 +448,7 @@ export class PortfolioCalculator {
transactionCount: item.transactionCount transactionCount: item.transactionCount
}); });
if (hasErrors) { if (hasErrors && item.investment.gt(0)) {
errors.push({ dataSource: item.dataSource, symbol: item.symbol }); errors.push({ dataSource: item.dataSource, symbol: item.symbol });
} }
} }

View File

@ -131,7 +131,8 @@ export class PortfolioController {
portfolioPosition.investment / totalInvestment; portfolioPosition.investment / totalInvestment;
portfolioPosition.netPerformance = null; portfolioPosition.netPerformance = null;
portfolioPosition.quantity = null; portfolioPosition.quantity = null;
portfolioPosition.value = portfolioPosition.value / totalValue; portfolioPosition.valueInPercentage =
portfolioPosition.value / totalValue;
} }
for (const [name, { current, original }] of Object.entries(accounts)) { for (const [name, { current, original }] of Object.entries(accounts)) {
@ -322,7 +323,7 @@ export class PortfolioController {
totalInvestment: new Big(totalInvestment) totalInvestment: new Big(totalInvestment)
.div(performanceInformation.performance.totalInvestment) .div(performanceInformation.performance.totalInvestment)
.toNumber(), .toNumber(),
value: new Big(value) valueInPercentage: new Big(value)
.div(performanceInformation.performance.currentValue) .div(performanceInformation.performance.currentValue)
.toNumber() .toNumber()
}; };
@ -356,6 +357,7 @@ export class PortfolioController {
@Get('positions') @Get('positions')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
@UseInterceptors(RedactValuesInResponseInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getPositions( public async getPositions(
@Headers('impersonation-id') impersonationId: string, @Headers('impersonation-id') impersonationId: string,
@ -370,27 +372,11 @@ export class PortfolioController {
filterByTags filterByTags
}); });
const result = await this.portfolioService.getPositions({ return this.portfolioService.getPositions({
dateRange, dateRange,
filters, filters,
impersonationId impersonationId
}); });
if (
impersonationId ||
this.userService.isRestrictedView(this.request.user)
) {
result.positions = result.positions.map((position) => {
return nullifyValuesInObject(position, [
'grossPerformance',
'investment',
'netPerformance',
'quantity'
]);
});
}
return result;
} }
@Get('public/:accessId') @Get('public/:accessId')
@ -441,7 +427,7 @@ export class PortfolioController {
for (const [symbol, portfolioPosition] of Object.entries(holdings)) { for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
portfolioPublicDetails.holdings[symbol] = { portfolioPublicDetails.holdings[symbol] = {
allocationCurrent: portfolioPosition.value / totalValue, allocationInPercentage: portfolioPosition.value / totalValue,
countries: hasDetails ? portfolioPosition.countries : [], countries: hasDetails ? portfolioPosition.countries : [],
currency: hasDetails ? portfolioPosition.currency : undefined, currency: hasDetails ? portfolioPosition.currency : undefined,
dataSource: portfolioPosition.dataSource, dataSource: portfolioPosition.dataSource,
@ -452,7 +438,7 @@ export class PortfolioController {
sectors: hasDetails ? portfolioPosition.sectors : [], sectors: hasDetails ? portfolioPosition.sectors : [],
symbol: portfolioPosition.symbol, symbol: portfolioPosition.symbol,
url: portfolioPosition.url, url: portfolioPosition.url,
value: portfolioPosition.value / totalValue valueInPercentage: portfolioPosition.value / totalValue
}; };
} }
@ -460,6 +446,7 @@ export class PortfolioController {
} }
@Get('position/:dataSource/:symbol') @Get('position/:dataSource/:symbol')
@UseInterceptors(RedactValuesInResponseInterceptor)
@UseInterceptors(TransformDataSourceInRequestInterceptor) @UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor)
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
@ -468,27 +455,13 @@ export class PortfolioController {
@Param('dataSource') dataSource, @Param('dataSource') dataSource,
@Param('symbol') symbol @Param('symbol') symbol
): Promise<PortfolioPositionDetail> { ): Promise<PortfolioPositionDetail> {
let position = await this.portfolioService.getPosition( const position = await this.portfolioService.getPosition(
dataSource, dataSource,
impersonationId, impersonationId,
symbol symbol
); );
if (position) { if (position) {
if (
impersonationId ||
this.userService.isRestrictedView(this.request.user)
) {
position = nullifyValuesInObject(position, [
'grossPerformance',
'investment',
'netPerformance',
'orders',
'quantity',
'value'
]);
}
return position; return position;
} }

View File

@ -24,7 +24,7 @@ import {
MAX_CHART_ITEMS, MAX_CHART_ITEMS,
UNKNOWN_KEY UNKNOWN_KEY
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper'; import { DATE_FORMAT, getSum, parseDate } from '@ghostfolio/common/helper';
import { import {
Accounts, Accounts,
EnhancedSymbolProfile, EnhancedSymbolProfile,
@ -537,12 +537,9 @@ export class PortfolioService {
holdings[item.symbol] = { holdings[item.symbol] = {
markets, markets,
allocationCurrent: filteredValueInBaseCurrency.eq(0) allocationInPercentage: filteredValueInBaseCurrency.eq(0)
? 0 ? 0
: value.div(filteredValueInBaseCurrency).toNumber(), : value.div(filteredValueInBaseCurrency).toNumber(),
allocationInvestment: item.investment
.div(totalInvestmentInBaseCurrency)
.toNumber(),
assetClass: symbolProfile.assetClass, assetClass: symbolProfile.assetClass,
assetSubClass: symbolProfile.assetSubClass, assetSubClass: symbolProfile.assetSubClass,
countries: symbolProfile.countries, countries: symbolProfile.countries,
@ -576,7 +573,6 @@ export class PortfolioService {
const cashPositions = await this.getCashPositions({ const cashPositions = await this.getCashPositions({
cashDetails, cashDetails,
userCurrency, userCurrency,
investment: totalInvestmentInBaseCurrency,
value: filteredValueInBaseCurrency value: filteredValueInBaseCurrency
}); });
@ -596,13 +592,12 @@ export class PortfolioService {
if ( if (
filters?.length === 1 && filters?.length === 1 &&
filters[0].id === 'EMERGENCY_FUND_TAG_ID' && filters[0].id === EMERGENCY_FUND_TAG_ID &&
filters[0].type === 'TAG' filters[0].type === 'TAG'
) { ) {
const cashPositions = await this.getCashPositions({ const cashPositions = await this.getCashPositions({
cashDetails, cashDetails,
userCurrency, userCurrency,
investment: totalInvestmentInBaseCurrency,
value: filteredValueInBaseCurrency value: filteredValueInBaseCurrency
}); });
@ -614,6 +609,8 @@ export class PortfolioService {
) )
.toNumber(); .toNumber();
filteredValueInBaseCurrency = emergencyFund;
accounts[UNKNOWN_KEY] = { accounts[UNKNOWN_KEY] = {
balance: 0, balance: 0,
currency: userCurrency, currency: userCurrency,
@ -681,6 +678,8 @@ export class PortfolioService {
return { return {
tags, tags,
averagePrice: undefined, averagePrice: undefined,
dividendInBaseCurrency: undefined,
feeInBaseCurrency: undefined,
firstBuyDate: undefined, firstBuyDate: undefined,
grossPerformance: undefined, grossPerformance: undefined,
grossPerformancePercent: undefined, grossPerformancePercent: undefined,
@ -747,12 +746,23 @@ export class PortfolioService {
averagePrice, averagePrice,
currency, currency,
dataSource, dataSource,
fee,
firstBuyDate, firstBuyDate,
marketPrice, marketPrice,
quantity, quantity,
transactionCount transactionCount
} = position; } = position;
const dividendInBaseCurrency = getSum(
orders
.filter(({ type }) => {
return type === 'DIVIDEND';
})
.map(({ valueInBaseCurrency }) => {
return new Big(valueInBaseCurrency);
})
);
// Convert investment, gross and net performance to currency of user // Convert investment, gross and net performance to currency of user
const investment = this.exchangeRateDataService.toCurrency( const investment = this.exchangeRateDataService.toCurrency(
position.investment?.toNumber(), position.investment?.toNumber(),
@ -786,8 +796,8 @@ export class PortfolioService {
historicalDataArray.push({ historicalDataArray.push({
averagePrice: orders[0].unitPrice, averagePrice: orders[0].unitPrice,
date: firstBuyDate, date: firstBuyDate,
quantity: orders[0].quantity, marketPrice: orders[0].unitPrice,
value: orders[0].unitPrice quantity: orders[0].quantity
}); });
} }
@ -816,9 +826,9 @@ export class PortfolioService {
historicalDataArray.push({ historicalDataArray.push({
date, date,
marketPrice,
averagePrice: currentAveragePrice, averagePrice: currentAveragePrice,
quantity: currentQuantity, quantity: currentQuantity
value: marketPrice
}); });
maxPrice = Math.max(marketPrice ?? 0, maxPrice); maxPrice = Math.max(marketPrice ?? 0, maxPrice);
@ -839,6 +849,12 @@ export class PortfolioService {
tags, tags,
transactionCount, transactionCount,
averagePrice: averagePrice.toNumber(), averagePrice: averagePrice.toNumber(),
dividendInBaseCurrency: dividendInBaseCurrency.toNumber(),
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
fee.toNumber(),
SymbolProfile.currency,
userCurrency
),
grossPerformancePercent: grossPerformancePercent:
position.grossPerformancePercentage?.toNumber(), position.grossPerformancePercentage?.toNumber(),
historicalData: historicalDataArray, historicalData: historicalDataArray,
@ -895,6 +911,8 @@ export class PortfolioService {
SymbolProfile, SymbolProfile,
tags, tags,
averagePrice: 0, averagePrice: 0,
dividendInBaseCurrency: 0,
feeInBaseCurrency: 0,
firstBuyDate: undefined, firstBuyDate: undefined,
grossPerformance: undefined, grossPerformance: undefined,
grossPerformancePercent: undefined, grossPerformancePercent: undefined,
@ -1040,29 +1058,21 @@ export class PortfolioService {
const portfolioStart = parseDate(transactionPoints[0].date); const portfolioStart = parseDate(transactionPoints[0].date);
const startDate = this.getStartDate(dateRange, portfolioStart); const startDate = this.getStartDate(dateRange, portfolioStart);
const currentPositions = await portfolioCalculator.getCurrentPositions( const {
startDate currentValue,
); errors,
grossPerformance,
grossPerformancePercentage,
hasErrors,
netPerformance,
netPerformancePercentage,
totalInvestment
} = await portfolioCalculator.getCurrentPositions(startDate);
const hasErrors = currentPositions.hasErrors; const currentGrossPerformance = grossPerformance;
const currentValue = currentPositions.currentValue.toNumber(); const currentGrossPerformancePercent = grossPerformancePercentage;
const currentGrossPerformance = currentPositions.grossPerformance; let currentNetPerformance = netPerformance;
const currentGrossPerformancePercent = let currentNetPerformancePercent = netPerformancePercentage;
currentPositions.grossPerformancePercentage;
let currentNetPerformance = currentPositions.netPerformance;
let currentNetPerformancePercent =
currentPositions.netPerformancePercentage;
const totalInvestment = currentPositions.totalInvestment;
// if (currentGrossPerformance.mul(currentGrossPerformancePercent).lt(0)) {
// // If algebraic sign is different, harmonize it
// currentGrossPerformancePercent = currentGrossPerformancePercent.mul(-1);
// }
// if (currentNetPerformance.mul(currentNetPerformancePercent).lt(0)) {
// // If algebraic sign is different, harmonize it
// currentNetPerformancePercent = currentNetPerformancePercent.mul(-1);
// }
const historicalDataContainer = await this.getChart({ const historicalDataContainer = await this.getChart({
dateRange, dateRange,
@ -1084,28 +1094,28 @@ export class PortfolioService {
} }
return { return {
errors,
hasErrors,
chart: historicalDataContainer.items.map( chart: historicalDataContainer.items.map(
({ ({
date, date,
netPerformance, netPerformance: netPerformanceOfItem,
netPerformanceInPercentage, netPerformanceInPercentage,
totalInvestment, totalInvestment: totalInvestmentOfItem,
value value
}) => { }) => {
return { return {
date, date,
netPerformance,
netPerformanceInPercentage, netPerformanceInPercentage,
totalInvestment, value,
value netPerformance: netPerformanceOfItem,
totalInvestment: totalInvestmentOfItem
}; };
} }
), ),
errors: currentPositions.errors,
firstOrderDate: parseDate(historicalDataContainer.items[0]?.date), firstOrderDate: parseDate(historicalDataContainer.items[0]?.date),
hasErrors: currentPositions.hasErrors || hasErrors,
performance: { performance: {
currentValue, currentValue: currentValue.toNumber(),
currentGrossPerformance: currentGrossPerformance.toNumber(), currentGrossPerformance: currentGrossPerformance.toNumber(),
currentGrossPerformancePercent: currentGrossPerformancePercent:
currentGrossPerformancePercent.toNumber(), currentGrossPerformancePercent.toNumber(),
@ -1218,12 +1228,10 @@ export class PortfolioService {
private async getCashPositions({ private async getCashPositions({
cashDetails, cashDetails,
investment,
userCurrency, userCurrency,
value value
}: { }: {
cashDetails: CashDetails; cashDetails: CashDetails;
investment: Big;
userCurrency: string; userCurrency: string;
value: Big; value: Big;
}) { }) {
@ -1258,12 +1266,9 @@ export class PortfolioService {
for (const symbol of Object.keys(cashPositions)) { for (const symbol of Object.keys(cashPositions)) {
// Calculate allocations for each currency // Calculate allocations for each currency
cashPositions[symbol].allocationCurrent = value.gt(0) cashPositions[symbol].allocationInPercentage = value.gt(0)
? new Big(cashPositions[symbol].value).div(value).toNumber() ? new Big(cashPositions[symbol].value).div(value).toNumber()
: 0; : 0;
cashPositions[symbol].allocationInvestment = investment.gt(0)
? new Big(cashPositions[symbol].investment).div(investment).toNumber()
: 0;
} }
return cashPositions; return cashPositions;
@ -1430,8 +1435,7 @@ export class PortfolioService {
}): PortfolioPosition { }): PortfolioPosition {
return { return {
currency, currency,
allocationCurrent: 0, allocationInPercentage: 0,
allocationInvestment: 0,
assetClass: AssetClass.CASH, assetClass: AssetClass.CASH,
assetSubClass: AssetClass.CASH, assetSubClass: AssetClass.CASH,
countries: [], countries: [],
@ -1705,6 +1709,14 @@ export class PortfolioService {
userId: string; userId: string;
withExcludedAccounts?: boolean; withExcludedAccounts?: boolean;
}) { }) {
const ordersOfTypeItem = await this.orderService.getOrders({
filters,
userCurrency,
userId,
withExcludedAccounts,
types: ['ITEM']
});
const accounts: PortfolioDetails['accounts'] = {}; const accounts: PortfolioDetails['accounts'] = {};
let currentAccounts: (Account & { let currentAccounts: (Account & {
@ -1735,10 +1747,18 @@ export class PortfolioService {
}); });
for (const account of currentAccounts) { for (const account of currentAccounts) {
const ordersByAccount = orders.filter(({ accountId }) => { let ordersByAccount = orders.filter(({ accountId }) => {
return accountId === account.id; return accountId === account.id;
}); });
const ordersOfTypeItemByAccount = ordersOfTypeItem.filter(
({ accountId }) => {
return accountId === account.id;
}
);
ordersByAccount = ordersByAccount.concat(ordersOfTypeItemByAccount);
accounts[account.id] = { accounts[account.id] = {
balance: account.balance, balance: account.balance,
currency: account.currency, currency: account.currency,
@ -1758,7 +1778,9 @@ export class PortfolioService {
for (const order of ordersByAccount) { for (const order of ordersByAccount) {
let currentValueOfSymbolInBaseCurrency = let currentValueOfSymbolInBaseCurrency =
order.quantity * order.quantity *
portfolioItemsNow[order.SymbolProfile.symbol]?.marketPrice ?? 0; (portfolioItemsNow[order.SymbolProfile.symbol]?.marketPrice ??
order.unitPrice ??
0);
let originalValueOfSymbolInBaseCurrency = let originalValueOfSymbolInBaseCurrency =
this.exchangeRateDataService.toCurrency( this.exchangeRateDataService.toCurrency(
order.quantity * order.unitPrice, order.quantity * order.unitPrice,

View File

@ -1,6 +1,4 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { PROPERTY_IS_USER_SIGNUP_ENABLED } from '@ghostfolio/common/config';
import { User, UserSettings } from '@ghostfolio/common/interfaces'; import { User, UserSettings } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types'; import type { RequestWithUser } from '@ghostfolio/common/types';
@ -31,7 +29,6 @@ import { UserService } from './user.service';
@Controller('user') @Controller('user')
export class UserController { export class UserController {
public constructor( public constructor(
private readonly configurationService: ConfigurationService,
private readonly jwtService: JwtService, private readonly jwtService: JwtService,
private readonly propertyService: PropertyService, private readonly propertyService: PropertyService,
@Inject(REQUEST) private readonly request: RequestWithUser, @Inject(REQUEST) private readonly request: RequestWithUser,

View File

@ -97,6 +97,7 @@ export class UserService {
const { const {
accessToken, accessToken,
Account, Account,
Analytics,
authChallenge, authChallenge,
createdAt, createdAt,
id, id,
@ -107,7 +108,12 @@ export class UserService {
thirdPartyId, thirdPartyId,
updatedAt updatedAt
} = await this.prismaService.user.findUnique({ } = await this.prismaService.user.findUnique({
include: { Account: true, Settings: true, Subscription: true }, include: {
Account: true,
Analytics: true,
Settings: true,
Subscription: true
},
where: userWhereUniqueInput where: userWhereUniqueInput
}); });
@ -121,7 +127,8 @@ export class UserService {
role, role,
Settings, Settings,
thirdPartyId, thirdPartyId,
updatedAt updatedAt,
activityCount: Analytics?.activityCount
}; };
if (user?.Settings) { if (user?.Settings) {
@ -154,16 +161,23 @@ export class UserService {
(user.Settings.settings as UserSettings).viewMode = 'DEFAULT'; (user.Settings.settings as UserSettings).viewMode = 'DEFAULT';
} }
let currentPermissions = getPermissions(user.role);
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
user.subscription = user.subscription =
this.subscriptionService.getSubscription(Subscription); this.subscriptionService.getSubscription(Subscription);
}
let currentPermissions = getPermissions(user.role); if (
Analytics?.activityCount % 25 === 0 &&
user.subscription?.type === 'Basic'
) {
currentPermissions.push(permissions.enableSubscriptionInterstitial);
}
if (user.subscription?.type === 'Premium') { if (user.subscription?.type === 'Premium') {
currentPermissions.push(permissions.reportDataGlitch); currentPermissions.push(permissions.reportDataGlitch);
} }
}
if (this.configurationService.get('ENABLE_FEATURE_READ_ONLY_MODE')) { if (this.configurationService.get('ENABLE_FEATURE_READ_ONLY_MODE')) {
if (hasRole(user, Role.ADMIN)) { if (hasRole(user, Role.ADMIN)) {

View File

@ -1,4 +1,4 @@
import { cloneDeep, isObject } from 'lodash'; import { cloneDeep, isArray, isObject } from 'lodash';
export function hasNotDefinedValuesInObject(aObject: Object): boolean { export function hasNotDefinedValuesInObject(aObject: Object): boolean {
for (const key in aObject) { for (const key in aObject) {
@ -43,15 +43,23 @@ export function redactAttributes({
for (const option of options) { for (const option of options) {
if (redactedObject.hasOwnProperty(option.attribute)) { if (redactedObject.hasOwnProperty(option.attribute)) {
if (option.valueMap['*'] || option.valueMap['*'] === null) {
redactedObject[option.attribute] = option.valueMap['*'];
} else if (option.valueMap[redactedObject[option.attribute]]) {
redactedObject[option.attribute] = redactedObject[option.attribute] =
option.valueMap[redactedObject[option.attribute]] ?? option.valueMap[redactedObject[option.attribute]];
option.valueMap['*'] ?? }
redactedObject[option.attribute];
} else { } else {
// If the attribute is not present on the current object, // If the attribute is not present on the current object,
// check if it exists on any nested objects // check if it exists on any nested objects
for (const property in redactedObject) { for (const property in redactedObject) {
if (typeof redactedObject[property] === 'object') { if (isArray(redactedObject[property])) {
redactedObject[property] = redactedObject[property].map(
(currentObject) => {
return redactAttributes({ options, object: currentObject });
}
);
} else if (isObject(redactedObject[property])) {
// Recursively call the function on the nested object // Recursively call the function on the nested object
redactedObject[property] = redactAttributes({ redactedObject[property] = redactAttributes({
options, options,

View File

@ -1,5 +1,5 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { UserService } from '@ghostfolio/api/app/user/user.service'; import { UserService } from '@ghostfolio/api/app/user/user.service';
import { redactAttributes } from '@ghostfolio/api/helper/object.helper';
import { import {
CallHandler, CallHandler,
ExecutionContext, ExecutionContext,
@ -28,61 +28,38 @@ export class RedactValuesInResponseInterceptor<T>
hasImpersonationId || hasImpersonationId ||
this.userService.isRestrictedView(request.user) this.userService.isRestrictedView(request.user)
) { ) {
if (data.accounts) { data = redactAttributes({
for (const accountId of Object.keys(data.accounts)) { object: data,
if (data.accounts[accountId]?.balance !== undefined) { options: [
data.accounts[accountId].balance = null; 'balance',
'balanceInBaseCurrency',
'comment',
'convertedBalance',
'dividendInBaseCurrency',
'fee',
'feeInBaseCurrency',
'filteredValueInBaseCurrency',
'grossPerformance',
'investment',
'netPerformance',
'quantity',
'symbolMapping',
'totalBalanceInBaseCurrency',
'totalValueInBaseCurrency',
'unitPrice',
'value',
'valueInBaseCurrency'
].map((attribute) => {
return {
attribute,
valueMap: {
'*': null
} }
} };
} })
if (data.activities) {
data.activities = data.activities.map((activity: Activity) => {
if (activity.Account?.balance !== undefined) {
activity.Account.balance = null;
}
if (activity.comment !== undefined) {
activity.comment = null;
}
if (activity.fee !== undefined) {
activity.fee = null;
}
if (activity.feeInBaseCurrency !== undefined) {
activity.feeInBaseCurrency = null;
}
if (activity.quantity !== undefined) {
activity.quantity = null;
}
if (activity.unitPrice !== undefined) {
activity.unitPrice = null;
}
if (activity.value !== undefined) {
activity.value = null;
}
if (activity.valueInBaseCurrency !== undefined) {
activity.valueInBaseCurrency = null;
}
return activity;
}); });
} }
if (data.filteredValueInBaseCurrency) {
data.filteredValueInBaseCurrency = null;
}
if (data.totalValueInBaseCurrency) {
data.totalValueInBaseCurrency = null;
}
}
return data; return data;
}) })
); );

View File

@ -42,7 +42,7 @@ export class ConfigurationService {
MAX_ITEM_IN_CACHE: num({ default: 9999 }), MAX_ITEM_IN_CACHE: num({ default: 9999 }),
PORT: port({ default: 3333 }), PORT: port({ default: 3333 }),
RAPID_API_API_KEY: str({ default: '' }), RAPID_API_API_KEY: str({ default: '' }),
REDIS_HOST: host({ default: 'localhost' }), REDIS_HOST: str({ default: 'localhost' }),
REDIS_PASSWORD: str({ default: '' }), REDIS_PASSWORD: str({ default: '' }),
REDIS_PORT: port({ default: 6379 }), REDIS_PORT: port({ default: 6379 }),
ROOT_URL: str({ default: 'http://localhost:4200' }), ROOT_URL: str({ default: 'http://localhost:4200' }),

View File

@ -1,18 +0,0 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR
not IE 9-10 # Angular support for IE 9-10 has been deprecated and will be removed as of Angular v11. To opt-in, remove the 'not' prefix on this line.
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.

View File

@ -65,7 +65,10 @@
"output": "./../assets/" "output": "./../assets/"
} }
], ],
"styles": ["apps/client/src/styles.scss"], "styles": [
"apps/client/src/styles/theme.scss",
"apps/client/src/styles.scss"
],
"scripts": ["node_modules/marked/marked.min.js"], "scripts": ["node_modules/marked/marked.min.js"],
"vendorChunk": true, "vendorChunk": true,
"extractLicenses": false, "extractLicenses": false,

View File

@ -116,6 +116,13 @@ const routes: Routes = [
'./pages/blog/2022/12/the-importance-of-tracking-your-personal-finances/the-importance-of-tracking-your-personal-finances-page.module' './pages/blog/2022/12/the-importance-of-tracking-your-personal-finances/the-importance-of-tracking-your-personal-finances-page.module'
).then((m) => m.TheImportanceOfTrackingYourPersonalFinancesPageModule) ).then((m) => m.TheImportanceOfTrackingYourPersonalFinancesPageModule)
}, },
{
path: 'blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt',
loadChildren: () =>
import(
'./pages/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt/ghostfolio-auf-sackgeld-vorgestellt-page.module'
).then((m) => m.GhostfolioAufSackgeldVorgestelltPageModule)
},
{ {
path: 'demo', path: 'demo',
loadChildren: () => loadChildren: () =>
@ -215,9 +222,8 @@ const routes: Routes = [
// Preload all lazy loaded modules with the attribute preload === true // Preload all lazy loaded modules with the attribute preload === true
{ {
anchorScrolling: 'enabled', anchorScrolling: 'enabled',
preloadingStrategy: ModulePreloadService, preloadingStrategy: ModulePreloadService
// enableTracing: true // <-- debugging purposes only // enableTracing: true // <-- debugging purposes only
relativeLinkResolution: 'legacy'
} }
) )
], ],

View File

@ -1,26 +1,17 @@
import { DOCUMENT } from '@angular/common';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
Inject,
OnDestroy, OnDestroy,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { import { NavigationEnd, PRIMARY_OUTLET, Router } from '@angular/router';
ActivatedRoute,
NavigationEnd,
PRIMARY_OUTLET,
Router
} from '@angular/router';
import {
primaryColorHex,
secondaryColorHex,
warnColorHex
} from '@ghostfolio/common/config';
import { InfoItem, User } from '@ghostfolio/common/interfaces'; import { InfoItem, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { ColorScheme } from '@ghostfolio/common/types'; import { ColorScheme } from '@ghostfolio/common/types';
import { MaterialCssVarsService } from 'angular-material-css-vars';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators'; import { filter, takeUntil } from 'rxjs/operators';
@ -52,7 +43,7 @@ export class AppComponent implements OnDestroy, OnInit {
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private materialCssVarsService: MaterialCssVarsService, @Inject(DOCUMENT) private document: Document,
private router: Router, private router: Router,
private title: Title, private title: Title,
private tokenStorageService: TokenStorageService, private tokenStorageService: TokenStorageService,
@ -126,16 +117,20 @@ export class AppComponent implements OnDestroy, OnInit {
? userPreferredColorScheme === 'DARK' ? userPreferredColorScheme === 'DARK'
: window.matchMedia('(prefers-color-scheme: dark)').matches; : window.matchMedia('(prefers-color-scheme: dark)').matches;
this.materialCssVarsService.setDarkTheme(isDarkTheme); this.toggleThemeStyleClass(isDarkTheme);
window.matchMedia('(prefers-color-scheme: dark)').addListener((event) => { window.matchMedia('(prefers-color-scheme: dark)').addListener((event) => {
if (!this.user?.settings.colorScheme) { if (!this.user?.settings.colorScheme) {
this.materialCssVarsService.setDarkTheme(event.matches); this.toggleThemeStyleClass(event.matches);
} }
}); });
}
this.materialCssVarsService.setPrimaryColor(primaryColorHex); private toggleThemeStyleClass(isDarkTheme: boolean) {
this.materialCssVarsService.setAccentColor(secondaryColorHex); if (isDarkTheme) {
this.materialCssVarsService.setWarnColor(warnColorHex); this.document.body.classList.add('is-dark-theme');
} else {
this.document.body.classList.remove('is-dark-theme');
}
} }
} }

View File

@ -1,20 +1,19 @@
import { Platform } from '@angular/cdk/platform'; import { Platform } from '@angular/cdk/platform';
import { HttpClientModule } from '@angular/common/http'; import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatChipsModule } from '@angular/material/chips';
import { import {
DateAdapter, DateAdapter,
MAT_DATE_FORMATS, MAT_DATE_FORMATS,
MAT_DATE_LOCALE, MAT_DATE_LOCALE,
MatNativeDateModule MatNativeDateModule
} from '@angular/material/core'; } from '@angular/material/core';
import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatLegacyAutocompleteModule as MatAutocompleteModule } from '@angular/material/legacy-autocomplete';
import { MatTooltipModule } from '@angular/material/tooltip'; import { MatLegacyChipsModule as MatChipsModule } from '@angular/material/legacy-chips';
import { MatLegacySnackBarModule as MatSnackBarModule } from '@angular/material/legacy-snack-bar';
import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ServiceWorkerModule } from '@angular/service-worker'; import { ServiceWorkerModule } from '@angular/service-worker';
import { MaterialCssVarsModule } from 'angular-material-css-vars';
import { MarkdownModule } from 'ngx-markdown'; import { MarkdownModule } from 'ngx-markdown';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { NgxStripeModule, STRIPE_PUBLISHABLE_KEY } from 'ngx-stripe'; import { NgxStripeModule, STRIPE_PUBLISHABLE_KEY } from 'ngx-stripe';
@ -25,6 +24,7 @@ import { DateFormats } from './adapter/date-formats';
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { GfHeaderModule } from './components/header/header.module'; import { GfHeaderModule } from './components/header/header.module';
import { GfSubscriptionInterstitialDialogModule } from './components/subscription-interstitial-dialog/subscription-interstitial-dialog.module';
import { authInterceptorProviders } from './core/auth.interceptor'; import { authInterceptorProviders } from './core/auth.interceptor';
import { httpResponseInterceptorProviders } from './core/http-response.interceptor'; import { httpResponseInterceptorProviders } from './core/http-response.interceptor';
import { LanguageService } from './core/language.service'; import { LanguageService } from './core/language.service';
@ -40,15 +40,11 @@ export function NgxStripeFactory(): string {
BrowserAnimationsModule, BrowserAnimationsModule,
BrowserModule, BrowserModule,
GfHeaderModule, GfHeaderModule,
GfSubscriptionInterstitialDialogModule,
HttpClientModule, HttpClientModule,
MarkdownModule.forRoot(), MarkdownModule.forRoot(),
MatAutocompleteModule, MatAutocompleteModule,
MatChipsModule, MatChipsModule,
MaterialCssVarsModule.forRoot({
darkThemeClass: 'is-dark-theme',
isAutoContrast: true,
lightThemeClass: 'is-light-theme'
}),
MatNativeDateModule, MatNativeDateModule,
MatSnackBarModule, MatSnackBarModule,
MatTooltipModule, MatTooltipModule,

View File

@ -7,7 +7,7 @@ import {
OnInit, OnInit,
Output Output
} from '@angular/core'; } from '@angular/core';
import { MatTableDataSource } from '@angular/material/table'; import { MatLegacyTableDataSource as MatTableDataSource } from '@angular/material/legacy-table';
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config'; import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
import { Access } from '@ghostfolio/common/interfaces'; import { Access } from '@ghostfolio/common/interfaces';

View File

@ -1,8 +1,8 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { MatMenuModule } from '@angular/material/menu'; import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu';
import { MatTableModule } from '@angular/material/table'; import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table';
import { AccessTableComponent } from './access-table.component'; import { AccessTableComponent } from './access-table.component';

View File

@ -6,7 +6,10 @@ import {
OnDestroy, OnDestroy,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import {
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
MatLegacyDialogRef as MatDialogRef
} from '@angular/material/legacy-dialog';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { downloadAsFile } from '@ghostfolio/common/helper'; import { downloadAsFile } from '@ghostfolio/common/helper';

View File

@ -1,7 +1,7 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { MatDialogModule } from '@angular/material/dialog'; import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module'; import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module'; import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module'; import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';

View File

@ -9,8 +9,8 @@ import {
Output, Output,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { MatLegacyTableDataSource as MatTableDataSource } from '@angular/material/legacy-table';
import { MatSort } from '@angular/material/sort'; import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Account as AccountModel } from '@prisma/client'; import { Account as AccountModel } from '@prisma/client';
import { get } from 'lodash'; import { get } from 'lodash';

View File

@ -1,10 +1,9 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { MatInputModule } from '@angular/material/input'; import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu';
import { MatMenuModule } from '@angular/material/menu'; import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table';
import { MatSortModule } from '@angular/material/sort'; import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module'; import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module';
import { GfValueModule } from '@ghostfolio/ui/value'; import { GfValueModule } from '@ghostfolio/ui/value';
@ -20,7 +19,6 @@ import { AccountsTableComponent } from './accounts-table.component';
GfSymbolIconModule, GfSymbolIconModule,
GfValueModule, GfValueModule,
MatButtonModule, MatButtonModule,
MatInputModule,
MatMenuModule, MatMenuModule,
MatSortModule, MatSortModule,
MatTableModule, MatTableModule,

View File

@ -1,9 +1,9 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { MatMenuModule } from '@angular/material/menu'; import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu';
import { MatSelectModule } from '@angular/material/select'; import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select';
import { AdminJobsComponent } from './admin-jobs.component'; import { AdminJobsComponent } from './admin-jobs.component';

View File

@ -7,7 +7,7 @@ import {
OnInit, OnInit,
Output Output
} from '@angular/core'; } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { import {
DATE_FORMAT, DATE_FORMAT,

View File

@ -6,7 +6,10 @@ import {
OnDestroy OnDestroy
} from '@angular/core'; } from '@angular/core';
import { DateAdapter, MAT_DATE_LOCALE } from '@angular/material/core'; import { DateAdapter, MAT_DATE_LOCALE } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import {
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
MatLegacyDialogRef as MatDialogRef
} from '@angular/material/legacy-dialog';
import { AdminService } from '@ghostfolio/client/services/admin.service'; import { AdminService } from '@ghostfolio/client/services/admin.service';
import { Subject, takeUntil } from 'rxjs'; import { Subject, takeUntil } from 'rxjs';
@ -36,7 +39,7 @@ export class MarketDataDetailDialog implements OnDestroy {
this.dateAdapter.setLocale(this.locale); this.dateAdapter.setLocale(this.locale);
} }
public onCancel(): void { public onCancel() {
this.dialogRef.close({ withRefresh: false }); this.dialogRef.close({ withRefresh: false });
} }

View File

@ -1,11 +1,11 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatDialogModule } from '@angular/material/dialog'; import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
import { MatInputModule } from '@angular/material/input'; import { MatLegacyFormFieldModule as MatFormFieldModule } from '@angular/material/legacy-form-field';
import { MatLegacyInputModule as MatInputModule } from '@angular/material/legacy-input';
import { MarketDataDetailDialog } from './market-data-detail-dialog.component'; import { MarketDataDetailDialog } from './market-data-detail-dialog.component';

View File

@ -6,9 +6,9 @@ import {
OnInit, OnInit,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog';
import { MatLegacyTableDataSource as MatTableDataSource } from '@angular/material/legacy-table';
import { MatSort } from '@angular/material/sort'; import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { AdminService } from '@ghostfolio/client/services/admin.service'; import { AdminService } from '@ghostfolio/client/services/admin.service';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';

View File

@ -1,13 +1,13 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { MatMenuModule } from '@angular/material/menu'; import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu';
import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table';
import { MatSortModule } from '@angular/material/sort'; import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { GfActivitiesFilterModule } from '@ghostfolio/ui/activities-filter/activities-filter.module'; import { GfActivitiesFilterModule } from '@ghostfolio/ui/activities-filter/activities-filter.module';
import { AdminMarketDataComponent } from './admin-market-data.component'; import { AdminMarketDataComponent } from './admin-market-data.component';
import { GfAssetProfileDialogModule } from './asset-profile-dialog/assset-profile-dialog.module'; import { GfAssetProfileDialogModule } from './asset-profile-dialog/asset-profile-dialog.module';
@NgModule({ @NgModule({
declarations: [AdminMarketDataComponent], declarations: [AdminMarketDataComponent],

View File

@ -7,7 +7,10 @@ import {
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { FormBuilder } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import {
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
MatLegacyDialogRef as MatDialogRef
} from '@angular/material/legacy-dialog';
import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto'; import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto';
import { AdminService } from '@ghostfolio/client/services/admin.service'; import { AdminService } from '@ghostfolio/client/services/admin.service';
import { import {

View File

@ -2,10 +2,10 @@ import { TextFieldModule } from '@angular/cdk/text-field';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { MatDialogModule } from '@angular/material/dialog'; import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
import { MatInputModule } from '@angular/material/input'; import { MatLegacyInputModule as MatInputModule } from '@angular/material/legacy-input';
import { MatMenuModule } from '@angular/material/menu'; import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu';
import { GfAdminMarketDataDetailModule } from '@ghostfolio/client/components/admin-market-data-detail/admin-market-data-detail.module'; import { GfAdminMarketDataDetailModule } from '@ghostfolio/client/components/admin-market-data-detail/admin-market-data-detail.module';
import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module'; import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module';
import { GfValueModule } from '@ghostfolio/ui/value'; import { GfValueModule } from '@ghostfolio/ui/value';

View File

@ -1,5 +1,5 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { MatSlideToggleChange } from '@angular/material/slide-toggle'; import { MatLegacySlideToggleChange as MatSlideToggleChange } from '@angular/material/legacy-slide-toggle';
import { CacheService } from '@ghostfolio/client/services/cache.service'; import { CacheService } from '@ghostfolio/client/services/cache.service';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';

View File

@ -1,10 +1,10 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { MatCardModule } from '@angular/material/card'; import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
import { MatSelectModule } from '@angular/material/select'; import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select';
import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { MatLegacySlideToggleModule as MatSlideToggleModule } from '@angular/material/legacy-slide-toggle';
import { CacheService } from '@ghostfolio/client/services/cache.service'; import { CacheService } from '@ghostfolio/client/services/cache.service';
import { GfValueModule } from '@ghostfolio/ui/value'; import { GfValueModule } from '@ghostfolio/ui/value';

View File

@ -1,7 +1,7 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { MatMenuModule } from '@angular/material/menu'; import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu';
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator'; import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
import { GfValueModule } from '@ghostfolio/ui/value'; import { GfValueModule } from '@ghostfolio/ui/value';

View File

@ -1,7 +1,7 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatSelectModule } from '@angular/material/select'; import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select';
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator'; import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';

View File

@ -1,6 +1,6 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { DialogFooterComponent } from './dialog-footer.component'; import { DialogFooterComponent } from './dialog-footer.component';

View File

@ -1,6 +1,6 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { DialogHeaderComponent } from './dialog-header.component'; import { DialogHeaderComponent } from './dialog-header.component';

View File

@ -6,7 +6,7 @@ import {
OnChanges, OnChanges,
Output Output
} from '@angular/core'; } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { LoginWithAccessTokenDialog } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.component'; import { LoginWithAccessTokenDialog } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.component';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';

View File

@ -1,7 +1,7 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { MatMenuModule } from '@angular/material/menu'; import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu';
import { MatToolbarModule } from '@angular/material/toolbar'; import { MatToolbarModule } from '@angular/material/toolbar';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { LoginWithAccessTokenDialogModule } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.module'; import { LoginWithAccessTokenDialogModule } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.module';

View File

@ -1,5 +1,5 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component'; import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component'; import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component';

View File

@ -1,7 +1,7 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { MatCardModule } from '@angular/material/card'; import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { GfPositionDetailDialogModule } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.module'; import { GfPositionDetailDialogModule } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.module';
import { GfPositionsModule } from '@ghostfolio/client/components/positions/positions.module'; import { GfPositionsModule } from '@ghostfolio/client/components/positions/positions.module';

View File

@ -110,13 +110,12 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
range: this.user?.settings?.dateRange range: this.user?.settings?.dateRange
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => { .subscribe(({ chart, errors, performance }) => {
this.errors = response.errors; this.errors = errors;
this.hasError = response.hasErrors; this.performance = performance;
this.performance = response.performance;
this.isLoadingPerformance = false; this.isLoadingPerformance = false;
this.historicalDataItems = response.chart.map( this.historicalDataItems = chart.map(
({ date, netPerformanceInPercentage }) => { ({ date, netPerformanceInPercentage }) => {
return { return {
date, date,

View File

@ -37,7 +37,6 @@
[baseCurrency]="user?.settings?.baseCurrency" [baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType" [deviceType]="deviceType"
[errors]="errors" [errors]="errors"
[hasError]="hasError"
[isAllTimeHigh]="isAllTimeHigh" [isAllTimeHigh]="isAllTimeHigh"
[isAllTimeLow]="isAllTimeLow" [isAllTimeLow]="isAllTimeLow"
[isLoading]="isLoadingPerformance" [isLoading]="isLoadingPerformance"

View File

@ -1,9 +1,9 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { import {
MatSnackBar, MatLegacySnackBar as MatSnackBar,
MatSnackBarRef, MatLegacySnackBarRef as MatSnackBarRef,
TextOnlySnackBar LegacyTextOnlySnackBar as TextOnlySnackBar
} from '@angular/material/snack-bar'; } from '@angular/material/legacy-snack-bar';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';

View File

@ -1,6 +1,6 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatCardModule } from '@angular/material/card'; import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { GfPortfolioSummaryModule } from '@ghostfolio/client/components/portfolio-summary/portfolio-summary.module'; import { GfPortfolioSummaryModule } from '@ghostfolio/client/components/portfolio-summary/portfolio-summary.module';

View File

@ -1,6 +1,9 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import { MatCheckboxChange } from '@angular/material/checkbox'; import { MatLegacyCheckboxChange as MatCheckboxChange } from '@angular/material/legacy-checkbox';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import {
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
MatLegacyDialogRef as MatDialogRef
} from '@angular/material/legacy-dialog';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { InternetIdentityService } from '@ghostfolio/client/services/internet-identity.service'; import { InternetIdentityService } from '@ghostfolio/client/services/internet-identity.service';
import { import {

View File

@ -2,11 +2,11 @@ import { TextFieldModule } from '@angular/cdk/text-field';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox';
import { MatDialogModule } from '@angular/material/dialog'; import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatLegacyFormFieldModule as MatFormFieldModule } from '@angular/material/legacy-form-field';
import { MatInputModule } from '@angular/material/input'; import { MatLegacyInputModule as MatInputModule } from '@angular/material/legacy-input';
import { GfDialogHeaderModule } from '../dialog-header/dialog-header.module'; import { GfDialogHeaderModule } from '../dialog-header/dialog-header.module';
import { LoginWithAccessTokenDialog } from './login-with-access-token-dialog.component'; import { LoginWithAccessTokenDialog } from './login-with-access-token-dialog.component';

View File

@ -3,14 +3,14 @@
<div <div
class="flex-grow-1 status text-muted text-right" class="flex-grow-1 status text-muted text-right"
[title]=" [title]="
hasError && !isLoading errors?.length > 0 && !isLoading
? 'Sorry! Our data provider partner is experiencing the hiccups.' ? 'Sorry! Our data provider partner is experiencing the hiccups.'
: '' : ''
" "
(click)="errors?.length > 0 && onShowErrors()" (click)="errors?.length > 0 && onShowErrors()"
> >
<ion-icon <ion-icon
*ngIf="hasError && !isLoading" *ngIf="errors?.length > 0 && !isLoading"
name="alert-circle-outline" name="alert-circle-outline"
></ion-icon> ></ion-icon>
</div> </div>

View File

@ -28,7 +28,6 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
@Input() baseCurrency: string; @Input() baseCurrency: string;
@Input() deviceType: string; @Input() deviceType: string;
@Input() errors: ResponseError['errors']; @Input() errors: ResponseError['errors'];
@Input() hasError: boolean;
@Input() isAllTimeHigh: boolean; @Input() isAllTimeHigh: boolean;
@Input() isAllTimeLow: boolean; @Input() isAllTimeLow: boolean;
@Input() isLoading: boolean; @Input() isLoading: boolean;

View File

@ -6,7 +6,10 @@ import {
OnDestroy, OnDestroy,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import {
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
MatLegacyDialogRef as MatDialogRef
} from '@angular/material/legacy-dialog';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper'; import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper';
import { import {
@ -37,6 +40,8 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
public countries: { public countries: {
[code: string]: { name: string; value: number }; [code: string]: { name: string; value: number };
}; };
public dividendInBaseCurrency: number;
public feeInBaseCurrency: number;
public firstBuyDate: string; public firstBuyDate: string;
public grossPerformance: number; public grossPerformance: number;
public grossPerformancePercent: number; public grossPerformancePercent: number;
@ -78,6 +83,8 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
.subscribe( .subscribe(
({ ({
averagePrice, averagePrice,
dividendInBaseCurrency,
feeInBaseCurrency,
firstBuyDate, firstBuyDate,
grossPerformance, grossPerformance,
grossPerformancePercent, grossPerformancePercent,
@ -98,7 +105,8 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
this.averagePrice = averagePrice; this.averagePrice = averagePrice;
this.benchmarkDataItems = []; this.benchmarkDataItems = [];
this.countries = {}; this.countries = {};
this.reportDataGlitchMail = `mailto:hi@ghostfol.io?Subject=Ghostfolio Data Glitch Report&body=Hello%0D%0DI would like to report a data glitch for%0D%0DSymbol: ${SymbolProfile?.symbol}%0DData Source: ${SymbolProfile?.dataSource}%0D%0DAdditional notes:%0D%0DCan you please take a look?%0D%0DKind regards`; this.dividendInBaseCurrency = dividendInBaseCurrency;
this.feeInBaseCurrency = feeInBaseCurrency;
this.firstBuyDate = firstBuyDate; this.firstBuyDate = firstBuyDate;
this.grossPerformance = grossPerformance; this.grossPerformance = grossPerformance;
this.grossPerformancePercent = grossPerformancePercent; this.grossPerformancePercent = grossPerformancePercent;
@ -111,7 +119,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
return { return {
date: historicalDataItem.date, date: historicalDataItem.date,
value: historicalDataItem.value value: historicalDataItem.marketPrice
}; };
} }
); );
@ -123,6 +131,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
this.netPerformancePercent = netPerformancePercent; this.netPerformancePercent = netPerformancePercent;
this.orders = orders; this.orders = orders;
this.quantity = quantity; this.quantity = quantity;
this.reportDataGlitchMail = `mailto:hi@ghostfol.io?Subject=Ghostfolio Data Glitch Report&body=Hello%0D%0DI would like to report a data glitch for%0D%0DSymbol: ${SymbolProfile?.symbol}%0DData Source: ${SymbolProfile?.dataSource}%0D%0DAdditional notes:%0D%0DCan you please take a look?%0D%0DKind regards`;
this.sectors = {}; this.sectors = {};
this.SymbolProfile = SymbolProfile; this.SymbolProfile = SymbolProfile;
this.tags = tags.map(({ id, name }) => { this.tags = tags.map(({ id, name }) => {

View File

@ -119,6 +119,26 @@
>Investment</gf-value >Investment</gf-value
> >
</div> </div>
<div class="col-6 mb-3">
<gf-value
i18n
size="medium"
[currency]="data.baseCurrency"
[locale]="data.locale"
[value]="dividendInBaseCurrency"
>Dividend</gf-value
>
</div>
<div class="col-6 mb-3">
<gf-value
i18n
size="medium"
[currency]="data.baseCurrency"
[locale]="data.locale"
[value]="feeInBaseCurrency"
>Fees</gf-value
>
</div>
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value <gf-value
i18n i18n

View File

@ -1,8 +1,8 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { MatChipsModule } from '@angular/material/chips'; import { MatLegacyChipsModule as MatChipsModule } from '@angular/material/legacy-chips';
import { MatDialogModule } from '@angular/material/dialog'; import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module'; import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module'; import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module'; import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';

View File

@ -1,6 +1,6 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatDialogModule } from '@angular/material/dialog'; import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module'; import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
import { GfTrendIndicatorModule } from '@ghostfolio/ui/trend-indicator'; import { GfTrendIndicatorModule } from '@ghostfolio/ui/trend-indicator';

View File

@ -1,6 +1,6 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info'; import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
import { GfPositionModule } from '../position/position.module'; import { GfPositionModule } from '../position/position.module';

View File

@ -1,7 +1,7 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { MatCardModule } from '@angular/material/card'; import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
import { GfRuleModule } from '@ghostfolio/client/components/rule/rule.module'; import { GfRuleModule } from '@ghostfolio/client/components/rule/rule.module';
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info'; import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';

View File

@ -0,0 +1 @@
export interface SubscriptionInterstitialDialogParams {}

View File

@ -0,0 +1,25 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import {
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
MatLegacyDialogRef as MatDialogRef
} from '@angular/material/legacy-dialog';
import { SubscriptionInterstitialDialogParams } from './interfaces/interfaces';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'd-flex flex-column flex-grow-1 h-100' },
selector: 'gf-subscription-interstitial-dialog',
styleUrls: ['./subscription-interstitial-dialog.scss'],
templateUrl: 'subscription-interstitial-dialog.html'
})
export class SubscriptionInterstitialDialog {
public constructor(
@Inject(MAT_DIALOG_DATA) public data: SubscriptionInterstitialDialogParams,
public dialogRef: MatDialogRef<SubscriptionInterstitialDialog>
) {}
public onCancel() {
this.dialogRef.close({});
}
}

View File

@ -0,0 +1,42 @@
<h1 class="align-items-center d-flex" mat-dialog-title>
<span>Ghostfolio Premium</span>
<gf-premium-indicator class="ml-1"></gf-premium-indicator>
</h1>
<div class="flex-grow-1" mat-dialog-content>
<p class="h5" i18n>
Are you an ambitious investor who needs the full picture?
</p>
<p i18n>
By upgrading to Ghostfolio Premium, you will get these additional features:
</p>
<ul class="list-unstyled mb-3">
<li class="align-items-center d-flex mb-1">
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
<span i18n>Portfolio Summary</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
<span i18n>Performance Benchmarks</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
<span i18n>Allocations</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
<span i18n>FIRE Calculator</span>
</li>
<li class="align-items-center d-flex mb-1">
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
<a i18n [routerLink]="['/features']">and more Features...</a>
</li>
</ul>
<p>Refine your personal investment strategy now.</p>
</div>
<div class="justify-content-end" mat-dialog-actions>
<button i18n mat-button (click)="onCancel()">Skip</button>
<a color="primary" mat-flat-button [routerLink]="['/pricing']">
<span i18n>Upgrade Plan</span>
<ion-icon class="ml-1" name="arrow-forward-outline"></ion-icon>
</a>
</div>

View File

@ -0,0 +1,21 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
import { RouterModule } from '@angular/router';
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
import { SubscriptionInterstitialDialog } from './subscription-interstitial-dialog.component';
@NgModule({
declarations: [SubscriptionInterstitialDialog],
imports: [
CommonModule,
GfPremiumIndicatorModule,
MatButtonModule,
MatDialogModule,
RouterModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfSubscriptionInterstitialDialogModule {}

View File

@ -0,0 +1,11 @@
:host {
display: block;
.mat-dialog-content {
max-height: unset;
ion-icon[name='checkmark-circle-outline'] {
color: rgba(var(--palette-accent-500), 1);
}
}
}

View File

@ -1,7 +1,7 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms'; import { ReactiveFormsModule } from '@angular/forms';
import { MatRadioModule } from '@angular/material/radio'; import { MatLegacyRadioModule as MatRadioModule } from '@angular/material/legacy-radio';
import { ToggleComponent } from './toggle.component'; import { ToggleComponent } from './toggle.component';

View File

@ -8,10 +8,10 @@ import {
} from '@angular/common/http'; } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { import {
MatSnackBar, MatLegacySnackBar as MatSnackBar,
MatSnackBarRef, MatLegacySnackBarRef as MatSnackBarRef,
TextOnlySnackBar LegacyTextOnlySnackBar as TextOnlySnackBar
} from '@angular/material/snack-bar'; } from '@angular/material/legacy-snack-bar';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';

View File

@ -42,9 +42,11 @@
href="https://twitter.com/ghostfolio_" href="https://twitter.com/ghostfolio_"
title="Tweet to Ghostfolio on Twitter" title="Tweet to Ghostfolio on Twitter"
>@ghostfolio_</a >@ghostfolio_</a
><ng-container *ngIf="hasPermissionForSubscription"
>, send an e-mail to >, send an e-mail to
<a href="mailto:hi@ghostfol.io" title="Send an e-mail" <a href="mailto:hi@ghostfol.io" title="Send an e-mail"
>hi@ghostfol.io</a >hi@ghostfol.io</a
></ng-container
> >
or open an issue at or open an issue at
<a <a

View File

@ -1,7 +1,7 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { MatCardModule } from '@angular/material/card'; import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
import { GfValueModule } from '@ghostfolio/ui/value'; import { GfValueModule } from '@ghostfolio/ui/value';
import { AboutPageRoutingModule } from './about-page-routing.module'; import { AboutPageRoutingModule } from './about-page-routing.module';

View File

@ -1,6 +1,6 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatCardModule } from '@angular/material/card'; import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
import { MarkdownModule } from 'ngx-markdown'; import { MarkdownModule } from 'ngx-markdown';
import { ChangelogPageRoutingModule } from './changelog-page-routing.module'; import { ChangelogPageRoutingModule } from './changelog-page-routing.module';

View File

@ -5,16 +5,16 @@ import {
OnInit, OnInit,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog';
import { import {
MatSlideToggle, MatLegacySlideToggle as MatSlideToggle,
MatSlideToggleChange MatLegacySlideToggleChange as MatSlideToggleChange
} from '@angular/material/slide-toggle'; } from '@angular/material/legacy-slide-toggle';
import { import {
MatSnackBar, MatLegacySnackBar as MatSnackBar,
MatSnackBarRef, MatLegacySnackBarRef as MatSnackBarRef,
TextOnlySnackBar LegacyTextOnlySnackBar as TextOnlySnackBar
} from '@angular/material/snack-bar'; } from '@angular/material/legacy-snack-bar';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto'; import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';

View File

@ -1,13 +1,12 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { MatCardModule } from '@angular/material/card'; import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
import { MatDialogModule } from '@angular/material/dialog'; import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatLegacyFormFieldModule as MatFormFieldModule } from '@angular/material/legacy-form-field';
import { MatInputModule } from '@angular/material/input'; import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select';
import { MatSelectModule } from '@angular/material/select'; import { MatLegacySlideToggleModule as MatSlideToggleModule } from '@angular/material/legacy-slide-toggle';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { GfPortfolioAccessTableModule } from '@ghostfolio/client/components/access-table/access-table.module'; import { GfPortfolioAccessTableModule } from '@ghostfolio/client/components/access-table/access-table.module';
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator'; import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
@ -31,7 +30,6 @@ import { GfCreateOrUpdateAccessDialogModule } from './create-or-update-access-di
MatCardModule, MatCardModule,
MatDialogModule, MatDialogModule,
MatFormFieldModule, MatFormFieldModule,
MatInputModule,
MatSelectModule, MatSelectModule,
MatSlideToggleModule, MatSlideToggleModule,
ReactiveFormsModule, ReactiveFormsModule,

View File

@ -4,7 +4,10 @@ import {
Inject, Inject,
OnDestroy OnDestroy
} from '@angular/core'; } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import {
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
MatLegacyDialogRef as MatDialogRef
} from '@angular/material/legacy-dialog';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { CreateOrUpdateAccessDialogParams } from './interfaces/interfaces'; import { CreateOrUpdateAccessDialogParams } from './interfaces/interfaces';
@ -26,7 +29,7 @@ export class CreateOrUpdateAccessDialog implements OnDestroy {
ngOnInit() {} ngOnInit() {}
public onCancel(): void { public onCancel() {
this.dialogRef.close(); this.dialogRef.close();
} }

View File

@ -1,11 +1,11 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { MatDialogModule } from '@angular/material/dialog'; import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatLegacyFormFieldModule as MatFormFieldModule } from '@angular/material/legacy-form-field';
import { MatInputModule } from '@angular/material/input'; import { MatLegacyInputModule as MatInputModule } from '@angular/material/legacy-input';
import { MatSelectModule } from '@angular/material/select'; import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select';
import { CreateOrUpdateAccessDialog } from './create-or-update-access-dialog.component'; import { CreateOrUpdateAccessDialog } from './create-or-update-access-dialog.component';

View File

@ -1,5 +1,5 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto'; import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto'; import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';

View File

@ -1,6 +1,6 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { GfAccountDetailDialogModule } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.module'; import { GfAccountDetailDialogModule } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.module';
import { GfAccountsTableModule } from '@ghostfolio/client/components/accounts-table/accounts-table.module'; import { GfAccountsTableModule } from '@ghostfolio/client/components/accounts-table/accounts-table.module';

View File

@ -4,7 +4,10 @@ import {
Inject, Inject,
OnDestroy OnDestroy
} from '@angular/core'; } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import {
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
MatLegacyDialogRef as MatDialogRef
} from '@angular/material/legacy-dialog';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { DataService } from '../../../services/data.service'; import { DataService } from '../../../services/data.service';
@ -36,7 +39,7 @@ export class CreateOrUpdateAccountDialog implements OnDestroy {
this.platforms = platforms; this.platforms = platforms;
} }
public onCancel(): void { public onCancel() {
this.dialogRef.close(); this.dialogRef.close();
} }

View File

@ -1,12 +1,12 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox';
import { MatDialogModule } from '@angular/material/dialog'; import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatLegacyFormFieldModule as MatFormFieldModule } from '@angular/material/legacy-form-field';
import { MatInputModule } from '@angular/material/input'; import { MatLegacyInputModule as MatInputModule } from '@angular/material/legacy-input';
import { MatSelectModule } from '@angular/material/select'; import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select';
import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.component'; import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.component';

View File

@ -1,9 +1,9 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { MatCardModule } from '@angular/material/card'; import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
import { MatMenuModule } from '@angular/material/menu'; import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu';
import { MatTabsModule } from '@angular/material/tabs'; import { MatLegacyTabsModule as MatTabsModule } from '@angular/material/legacy-tabs';
import { GfAdminJobsModule } from '@ghostfolio/client/components/admin-jobs/admin-jobs.module'; import { GfAdminJobsModule } from '@ghostfolio/client/components/admin-jobs/admin-jobs.module';
import { GfAdminMarketDataModule } from '@ghostfolio/client/components/admin-market-data/admin-market-data.module'; import { GfAdminMarketDataModule } from '@ghostfolio/client/components/admin-market-data/admin-market-data.module';
import { GfAdminOverviewModule } from '@ghostfolio/client/components/admin-overview/admin-overview.module'; import { GfAdminOverviewModule } from '@ghostfolio/client/components/admin-overview/admin-overview.module';

View File

@ -7,7 +7,7 @@
<div class="mb-3 text-muted"><small>2022-07-23</small></div> <div class="mb-3 text-muted"><small>2022-07-23</small></div>
<img <img
alt="Ghostfolio meets Internet Identity Teaser" alt="Ghostfolio meets Internet Identity Teaser"
class="rounded w-100" class="border rounded w-100"
src="../assets/images/blog/ghostfolio-meets-internet-identity.png" src="../assets/images/blog/ghostfolio-meets-internet-identity.png"
title="Ghostfolio meets Internet Identity" title="Ghostfolio meets Internet Identity"
/> />

View File

@ -1,6 +1,6 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator'; import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';

View File

@ -1,6 +1,6 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { TheImportanceOfTrackingYourPersonalFinancesRoutingModule } from './the-importance-of-tracking-your-personal-finances-page-routing.module'; import { TheImportanceOfTrackingYourPersonalFinancesRoutingModule } from './the-importance-of-tracking-your-personal-finances-page-routing.module';

View File

@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { GhostfolioAufSackgeldVorgestelltPageComponent } from './ghostfolio-auf-sackgeld-vorgestellt-page.component';
const routes: Routes = [
{
canActivate: [AuthGuard],
component: GhostfolioAufSackgeldVorgestelltPageComponent,
path: '',
title: 'Ghostfolio auf Sackgeld.com vorgestellt'
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class GhostfolioAufSackgeldVorgestelltPageRoutingModule {}

View File

@ -0,0 +1,9 @@
import { Component } from '@angular/core';
@Component({
host: { class: 'page' },
selector: 'gf-ghostfolio-auf-sackgeld-vorgestellt-page',
styleUrls: ['./ghostfolio-auf-sackgeld-vorgestellt-page.scss'],
templateUrl: './ghostfolio-auf-sackgeld-vorgestellt-page.html'
})
export class GhostfolioAufSackgeldVorgestelltPageComponent {}

View File

@ -0,0 +1,178 @@
<div class="blog container">
<div class="row">
<div class="col-md-8 offset-md-2">
<article>
<div class="mb-4 text-center">
<h1 class="mb-1">Ghostfolio auf Sackgeld.com vorgestellt</h1>
<div class="mb-3 text-muted"><small>2023-01-21</small></div>
<img
alt="Ghostfolio auf Sackgeld.com vorgestellt Teaser"
class="border rounded w-100"
src="../assets/images/blog/ghostfolio-x-sackgeld.png"
title="Ghostfolio auf Sackgeld.com vorgestellt"
/>
</div>
<section class="mb-4">
<p>
Wir freuen uns darüber, dass unsere Open Source Portfolio Tracking
Software <a href="https://ghostfol.io">Ghostfolio</a> auf dem
FinTech Newsportal <i>Sackgeld.com</i> vorgestellt wurde.
</p>
<div class="container my-4">
<div class="row">
<div class="col-md-10 offset-md-1">
<blockquote class="blockquote m-0">
<p class="mb-0">
«Ghostfolio ist ein umfassender Portfolio Performance
Tracker der einfach zu bedienen ist, mit einigen sehr
innovativen Features aufwartet und echten Mehrwert für den
Investor bringt.»
</p>
</blockquote>
</div>
</div>
</div>
<p>
Im ausführlichen Bericht wird die Funktionsweise von Ghostfolio
erläutert, die unterstützten Assets aufgeführt sowie die
Preisstruktur im Vergleich zu anderen Anbietern dargelegt.
</p>
</section>
<section class="mb-4">
<h2 class="h4">
Ghostfolio Open Source Wealth Management Software
</h2>
<p>
Ghostfolio ermöglicht es dir, dein Portfolio einfach zu verfolgen
und zu analysieren. Es bietet dir detaillierte Informationen über
deine Positionen, historische Entwicklung, Performance und die
Zusammenstellung deines Portfolios. Durch die Open Source-Lizenz (<a
href="https://github.com/ghostfolio/ghostfolio/blob/main/LICENSE"
target="_blank"
>GNU Affero General Public License v3.0</a
>) wird die Software ständig weiterentwickelt, verbessert und du
hast sogar selbst die Möglichkeit, dich daran zu beteiligen. Wir
sind davon überzeugt, mit diesem Open-Source-Ansatz von Ghostfolio
das Finanzwissen und Investieren für alle zugänglicher zu machen.
</p>
</section>
<section class="mb-4">
<h2 class="h4">Sackgeld.com App für ein höheres Sackgeld</h2>
<p>
Das Schweizer FinTech Nachrichtenportal
<a href="https://www.sackgeld.com" target="_blank">Sackgeld.com</a>
informiert über die neuesten Entwicklungen und Innovationen im
Bereich FinTech. Dazu gehören News, Artikel und persönliche
Erfahrungen aus der Welt der digitalen Finanz Apps, Säule 3a, P2P
und Immobilien.
</p>
</section>
<section class="mb-4">
<p>
Wenn du mehr über Ghostfolio erfahren möchtest, kannst du hier den
ganzen Artikel nachlesen:
<a
href="https://www.sackgeld.com/was-taugt-ghostfolio-als-portfolio-performance-tracking-tool"
target="_blank"
>Was taugt Ghostfolio als Portfolio Performance Tracking-Tool?</a
>
</p>
</section>
<section class="mb-4">
<ul class="list-inline">
<li class="list-inline-item">
<span class="badge badge-light">AGPL-3.0</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Aktie</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Altersvorsorge</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Anlage</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">App</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Asset</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Feedback</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Finanzwissen</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Fintech</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Ghostfolio</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Immobilien</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Innovation</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Investieren</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Lizenz</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Media</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Open Source</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">OSS</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">P2P</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Performance</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Portfolio</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Presse</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Sackgeld</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Säule 3a</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Schweiz</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Software</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Taschengeld</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Tool</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Vermögen</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Vorsorge</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Wealth Management</span>
</li>
</ul>
</section>
</article>
</div>
</div>
</div>

View File

@ -0,0 +1,17 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { GhostfolioAufSackgeldVorgestelltPageRoutingModule } from './ghostfolio-auf-sackgeld-vorgestellt-page-routing.module';
import { GhostfolioAufSackgeldVorgestelltPageComponent } from './ghostfolio-auf-sackgeld-vorgestellt-page.component';
@NgModule({
declarations: [GhostfolioAufSackgeldVorgestelltPageComponent],
imports: [
CommonModule,
GhostfolioAufSackgeldVorgestelltPageRoutingModule,
RouterModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GhostfolioAufSackgeldVorgestelltPageModule {}

View File

@ -2,6 +2,32 @@
<div class="mb-5 row"> <div class="mb-5 row">
<div class="col"> <div class="col">
<h3 class="d-none d-sm-block mb-3 text-center" i18n>Blog</h3> <h3 class="d-none d-sm-block mb-3 text-center" i18n>Blog</h3>
<mat-card class="mb-3">
<mat-card-content>
<div class="container p-0">
<div class="flex-nowrap no-gutters row">
<a
class="d-flex overflow-hidden w-100"
href="../de/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt"
>
<div class="flex-grow-1 overflow-hidden">
<div class="h6 m-0 text-truncate">
Ghostfolio auf Sackgeld.com vorgestellt
</div>
<div class="d-flex text-muted">2023-01-21</div>
</div>
<div class="align-items-center d-flex">
<ion-icon
class="chevron text-muted"
name="chevron-forward-outline"
size="small"
></ion-icon>
</div>
</a>
</div>
</div>
</mat-card-content>
</mat-card>
<mat-card class="mb-3"> <mat-card class="mb-3">
<mat-card-content> <mat-card-content>
<div class="container p-0"> <div class="container p-0">

View File

@ -1,6 +1,6 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatCardModule } from '@angular/material/card'; import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
import { BlogPageRoutingModule } from './blog-page-routing.module'; import { BlogPageRoutingModule } from './blog-page-routing.module';
import { BlogPageComponent } from './blog-page.component'; import { BlogPageComponent } from './blog-page.component';

View File

@ -1,6 +1,6 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatCardModule } from '@angular/material/card'; import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
import { FaqPageRoutingModule } from './faq-page-routing.module'; import { FaqPageRoutingModule } from './faq-page-routing.module';
import { FaqPageComponent } from './faq-page.component'; import { FaqPageComponent } from './faq-page.component';

View File

@ -1,7 +1,7 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button';
import { MatCardModule } from '@angular/material/card'; import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card';
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator'; import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
import { FeaturesPageRoutingModule } from './features-page-routing.module'; import { FeaturesPageRoutingModule } from './features-page-routing.module';

View File

@ -1,6 +1,6 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatTabsModule } from '@angular/material/tabs'; import { MatLegacyTabsModule as MatTabsModule } from '@angular/material/legacy-tabs';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { GfHomeHoldingsModule } from '@ghostfolio/client/components/home-holdings/home-holdings.module'; import { GfHomeHoldingsModule } from '@ghostfolio/client/components/home-holdings/home-holdings.module';
import { GfHomeMarketModule } from '@ghostfolio/client/components/home-market/home-market.module'; import { GfHomeMarketModule } from '@ghostfolio/client/components/home-market/home-market.module';

View File

@ -106,7 +106,7 @@
<div class="row mb-5"> <div class="row mb-5">
<div class="col-12 text-center text-muted"><small>As seen in</small></div> <div class="col-12 text-center text-muted"><small>As seen in</small></div>
<div class="col-md-2 d-flex justify-content-center my-1"> <div class="col-md-3 d-flex justify-content-center my-1">
<a <a
class="d-block logo logo-alternative-to mask" class="d-block logo logo-alternative-to mask"
href="https://alternativeto.net" href="https://alternativeto.net"
@ -114,7 +114,7 @@
title="AlternativeTo - Crowdsourced software recommendations" title="AlternativeTo - Crowdsourced software recommendations"
></a> ></a>
</div> </div>
<div class="col-md-2 d-flex justify-content-center my-1"> <div class="col-md-3 d-flex justify-content-center my-1">
<a <a
class="d-block logo logo-awesome" class="d-block logo logo-awesome"
href="https://github.com/awesome-selfhosted/awesome-selfhosted" href="https://github.com/awesome-selfhosted/awesome-selfhosted"
@ -122,7 +122,7 @@
title="Awesome-Selfhosted: A list of Free Software network services and web applications which can be hosted on your own servers" title="Awesome-Selfhosted: A list of Free Software network services and web applications which can be hosted on your own servers"
></a> ></a>
</div> </div>
<div class="col-md-2 d-flex justify-content-center my-1"> <div class="col-md-3 d-flex justify-content-center my-1">
<a <a
class="d-block logo logo-openstartup" class="d-block logo logo-openstartup"
href="https://openstartup.tm" href="https://openstartup.tm"
@ -130,7 +130,7 @@
title="Open Startup: The most complete list of open startups" title="Open Startup: The most complete list of open startups"
></a> ></a>
</div> </div>
<div class="col-md-2 d-flex justify-content-center my-1"> <div class="col-md-3 d-flex justify-content-center my-1">
<a <a
class="d-block logo logo-privacy-tools mask" class="d-block logo logo-privacy-tools mask"
href="https://www.privacytools.io" href="https://www.privacytools.io"
@ -138,7 +138,7 @@
title="Privacy Tools: Software Alternatives and Encryption" title="Privacy Tools: Software Alternatives and Encryption"
></a> ></a>
</div> </div>
<div class="col-md-2 d-flex justify-content-center my-1"> <div class="col-md-3 d-flex justify-content-center my-1">
<a <a
class="d-block logo logo-product-hunt" class="d-block logo logo-product-hunt"
href="https://www.producthunt.com" href="https://www.producthunt.com"
@ -146,7 +146,23 @@
title="Product Hunt The best new products in tech." title="Product Hunt The best new products in tech."
></a> ></a>
</div> </div>
<div class="col-md-2 d-flex justify-content-center my-1"> <div class="col-md-3 d-flex justify-content-center my-1">
<a
class="d-block logo logo-sackgeld mask"
href="https://www.sackgeld.com"
target="_blank"
title="Sackgeld.com Apps für ein höheres Sackgeld"
></a>
</div>
<div class="col-md-3 d-flex justify-content-center my-1">
<a
class="d-block logo logo-sourceforge mask"
href="https://sourceforge.net"
target="_blank"
title="SourceForge: The Complete Open-Source and Business Software Platform"
></a>
</div>
<div class="col-md-3 d-flex justify-content-center my-1">
<a <a
class="d-block logo logo-unraid mask" class="d-block logo logo-unraid mask"
href="https://unraid.net" href="https://unraid.net"

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