Compare commits
31 Commits
Author | SHA1 | Date | |
---|---|---|---|
a3a9957196 | |||
9072cbdba1 | |||
120b691336 | |||
bd4ad76953 | |||
94d56f553f | |||
ecdd325228 | |||
51fbc538ca | |||
39a76f7f40 | |||
e4d325daab | |||
b765df65d6 | |||
c7b7efae3b | |||
be5b58f49a | |||
91c748c7ad | |||
ecfe694f0b | |||
1491bf7f76 | |||
b3b9a051c3 | |||
bf1146bfd6 | |||
0774ca91a1 | |||
f403807f2d | |||
f22991b090 | |||
1135a5b335 | |||
d9ea255c17 | |||
2c19d8c8e7 | |||
db090229ce | |||
fbe590ddb9 | |||
0d65136a9e | |||
dea87cc3cf | |||
a062a3cee4 | |||
5b1b207a6f | |||
63cc7b2871 | |||
3986e8f879 |
63
CHANGELOG.md
63
CHANGELOG.md
@ -5,6 +5,69 @@ 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.27.0 - 18.07.2021
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the onboarding
|
||||||
|
- Flow of creating a new account
|
||||||
|
- Info message to add the first transaction
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the chart on the landing page
|
||||||
|
- Fixed the url to the _Fear & Greed Index_ on the resources page
|
||||||
|
|
||||||
|
## 1.26.0 - 17.07.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the import functionality for transactions
|
||||||
|
- Added the `robots.txt` file
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the styling of the current pricing plan
|
||||||
|
- Improved the styling of the transaction type badge
|
||||||
|
- Set the public _Stripe_ key dynamically
|
||||||
|
- Upgraded `angular-material-css-vars` from version `2.0.0` to `2.1.0`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the warn color (button) of the theme
|
||||||
|
|
||||||
|
## 1.25.0 - 11.07.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the export functionality for transactions
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Respected the cash balance on the analysis page
|
||||||
|
- Improved the settings selectors on the account page
|
||||||
|
- Harmonized the slogan to "Open Source Wealth Management Software"
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed rendering of currency and platform in dialogs (account and transaction)
|
||||||
|
- Fixed an issue in the calculation of the average buy prices in the position detail chart
|
||||||
|
|
||||||
|
## 1.24.0 - 07.07.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the total value in the create or edit transaction dialog
|
||||||
|
- Added a balance attribute to the account model
|
||||||
|
- Calculated the total balance (cash)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Upgraded `@angular/cdk` and `@angular/material` from version `11.0.4` to `12.0.6`
|
||||||
|
- Upgraded `@nestjs` dependencies
|
||||||
|
- Upgraded `angular-material-css-vars` from version `1.2.0` to `2.0.0`
|
||||||
|
- Upgraded `Nx` from version `12.3.6` to `12.5.4`
|
||||||
|
|
||||||
## 1.23.1 - 03.07.2021
|
## 1.23.1 - 03.07.2021
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
25
README.md
25
README.md
@ -1,13 +1,22 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
|
<a href="https://ghostfol.io">
|
||||||
|
<img
|
||||||
|
alt="Ghostfolio Logo"
|
||||||
|
src="https://avatars.githubusercontent.com/u/82473144?s=200"
|
||||||
|
width="100"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
|
||||||
<h1>Ghostfolio</h1>
|
<h1>Ghostfolio</h1>
|
||||||
<p>
|
<p>
|
||||||
<strong>Open Source Portfolio Tracker</strong>
|
<strong>Open Source Wealth Management Software made for Humans</strong>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="https://ghostfol.io"><strong>Live Demo</strong></a>
|
<a href="https://ghostfol.io"><strong>Live Demo</strong></a>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<img src="https://img.shields.io/badge/contributions-welcome-orange.svg"/>
|
<a href="#contributing">
|
||||||
|
<img src="https://img.shields.io/badge/contributions-welcome-orange.svg"/></a>
|
||||||
<a href="https://travis-ci.com/github/ghostfolio/ghostfolio" rel="nofollow">
|
<a href="https://travis-ci.com/github/ghostfolio/ghostfolio" rel="nofollow">
|
||||||
<img src="https://travis-ci.com/ghostfolio/ghostfolio.svg?branch=main" alt="Build Status"/></a>
|
<img src="https://travis-ci.com/ghostfolio/ghostfolio.svg?branch=main" alt="Build Status"/></a>
|
||||||
<a href="https://www.gnu.org/licenses/agpl-3.0" rel="nofollow">
|
<a href="https://www.gnu.org/licenses/agpl-3.0" rel="nofollow">
|
||||||
@ -15,7 +24,13 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
**Ghostfolio** is an open source portfolio tracker based on web technology. The software empowers busy folks to have a sharp look of their financial assets and to make solid, data-driven investment decisions by evaluating automated static portfolio analysis rules.
|
**Ghostfolio** is an open source wealth management software built with web technology. The application empowers busy people to keep track of their wealth like stocks, ETFs or cryptocurrencies and make solid, data-driven investment decisions.
|
||||||
|
|
||||||
|
## Ghostfolio Premium
|
||||||
|
|
||||||
|
Our official **[Ghostfolio Premium](https://ghostfol.io/pricing)** cloud offering is the easiest way to get started. Due to the time it saves, this will be the best option for most people. The revenue is used for covering the hosting costs.
|
||||||
|
|
||||||
|
If you prefer to run Ghostfolio on your own infrastructure, please find the source code and further instructions here on _GitHub_.
|
||||||
|
|
||||||
## Why Ghostfolio?
|
## Why Ghostfolio?
|
||||||
|
|
||||||
@ -79,8 +94,8 @@ The frontend is built with [Angular](https://angular.io) and uses [Angular Mater
|
|||||||
1. Run `yarn setup:database` to initialize the database schema and populate your database with (example) data
|
1. Run `yarn setup:database` to initialize the database schema and populate your database with (example) data
|
||||||
1. Start server and client (see [_Development_](#Development))
|
1. Start server and client (see [_Development_](#Development))
|
||||||
1. Login as _Admin_ with the following _Security Token_: `ae76872ae8f3419c6d6f64bf51888ecbcc703927a342d815fafe486acdb938da07d0cf44fca211a0be74a423238f535362d390a41e81e633a9ce668a6e31cdf9`
|
1. Login as _Admin_ with the following _Security Token_: `ae76872ae8f3419c6d6f64bf51888ecbcc703927a342d815fafe486acdb938da07d0cf44fca211a0be74a423238f535362d390a41e81e633a9ce668a6e31cdf9`
|
||||||
1. Go to the _Admin Control Panel_ and press _Gather All Data_ to fetch historical data
|
1. Go to the _Admin Control Panel_ and click _Gather All Data_ to fetch historical data
|
||||||
1. Press _Sign out_ and check out the _Live Demo_
|
1. Click _Sign out_ and check out the _Live Demo_
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
@ -103,6 +103,11 @@
|
|||||||
"input": "",
|
"input": "",
|
||||||
"output": "./"
|
"output": "./"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"glob": "robots.txt",
|
||||||
|
"input": "apps/client/src/assets",
|
||||||
|
"output": "./"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"glob": "sitemap.xml",
|
"glob": "sitemap.xml",
|
||||||
"input": "apps/client/src/assets",
|
"input": "apps/client/src/assets",
|
||||||
|
@ -11,5 +11,6 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
moduleFileExtensions: ['ts', 'js', 'html'],
|
moduleFileExtensions: ['ts', 'js', 'html'],
|
||||||
coverageDirectory: '../../coverage/apps/api',
|
coverageDirectory: '../../coverage/apps/api',
|
||||||
testTimeout: 10000
|
testTimeout: 10000,
|
||||||
|
testEnvironment: 'node'
|
||||||
};
|
};
|
||||||
|
@ -4,6 +4,7 @@ import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alph
|
|||||||
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
||||||
import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
|
import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
|
||||||
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
|
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
|
||||||
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
@ -20,6 +21,7 @@ import { AccountService } from './account.service';
|
|||||||
AlphaVantageService,
|
AlphaVantageService,
|
||||||
ConfigurationService,
|
ConfigurationService,
|
||||||
DataProviderService,
|
DataProviderService,
|
||||||
|
ExchangeRateDataService,
|
||||||
GhostfolioScraperApiService,
|
GhostfolioScraperApiService,
|
||||||
ImpersonationService,
|
ImpersonationService,
|
||||||
PrismaService,
|
PrismaService,
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Account, Order, Prisma } from '@prisma/client';
|
import { Account, Currency, Order, Prisma } from '@prisma/client';
|
||||||
|
|
||||||
import { RedisCacheService } from '../redis-cache/redis-cache.service';
|
import { RedisCacheService } from '../redis-cache/redis-cache.service';
|
||||||
|
import { CashDetails } from './interfaces/cash-details.interface';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AccountService {
|
export class AccountService {
|
||||||
public constructor(
|
public constructor(
|
||||||
|
private exchangeRateDataService: ExchangeRateDataService,
|
||||||
private readonly redisCacheService: RedisCacheService,
|
private readonly redisCacheService: RedisCacheService,
|
||||||
private prisma: PrismaService
|
private prisma: PrismaService
|
||||||
) {}
|
) {}
|
||||||
@ -73,6 +76,27 @@ export class AccountService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getCashDetails(
|
||||||
|
aUserId: string,
|
||||||
|
aCurrency: Currency
|
||||||
|
): Promise<CashDetails> {
|
||||||
|
let totalCashBalance = 0;
|
||||||
|
|
||||||
|
const accounts = await this.accounts({
|
||||||
|
where: { userId: aUserId }
|
||||||
|
});
|
||||||
|
|
||||||
|
accounts.forEach((account) => {
|
||||||
|
totalCashBalance += this.exchangeRateDataService.toCurrency(
|
||||||
|
account.balance,
|
||||||
|
account.currency,
|
||||||
|
aCurrency
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { accounts, balance: totalCashBalance };
|
||||||
|
}
|
||||||
|
|
||||||
public async updateAccount(
|
public async updateAccount(
|
||||||
params: {
|
params: {
|
||||||
where: Prisma.AccountWhereUniqueInput;
|
where: Prisma.AccountWhereUniqueInput;
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
import { AccountType } from '@prisma/client';
|
import { AccountType, Currency } from '@prisma/client';
|
||||||
import { IsString, ValidateIf } from 'class-validator';
|
import { IsNumber, IsString, ValidateIf } from 'class-validator';
|
||||||
|
|
||||||
export class CreateAccountDto {
|
export class CreateAccountDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
accountType: AccountType;
|
accountType: AccountType;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
balance: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
currency: Currency;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
import { Account } from '@prisma/client';
|
||||||
|
|
||||||
|
export interface CashDetails {
|
||||||
|
accounts: Account[];
|
||||||
|
balance: number;
|
||||||
|
}
|
@ -1,10 +1,16 @@
|
|||||||
import { AccountType } from '@prisma/client';
|
import { AccountType, Currency } from '@prisma/client';
|
||||||
import { IsString, ValidateIf } from 'class-validator';
|
import { IsNumber, IsString, ValidateIf } from 'class-validator';
|
||||||
|
|
||||||
export class UpdateAccountDto {
|
export class UpdateAccountDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
accountType: AccountType;
|
accountType: AccountType;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
balance: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
currency: Currency;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
|
@ -23,6 +23,8 @@ import { AppController } from './app.controller';
|
|||||||
import { AuthModule } from './auth/auth.module';
|
import { AuthModule } from './auth/auth.module';
|
||||||
import { CacheModule } from './cache/cache.module';
|
import { CacheModule } from './cache/cache.module';
|
||||||
import { ExperimentalModule } from './experimental/experimental.module';
|
import { ExperimentalModule } from './experimental/experimental.module';
|
||||||
|
import { ExportModule } from './export/export.module';
|
||||||
|
import { ImportModule } from './import/import.module';
|
||||||
import { InfoModule } from './info/info.module';
|
import { InfoModule } from './info/info.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';
|
||||||
@ -41,6 +43,8 @@ import { UserModule } from './user/user.module';
|
|||||||
CacheModule,
|
CacheModule,
|
||||||
ConfigModule.forRoot(),
|
ConfigModule.forRoot(),
|
||||||
ExperimentalModule,
|
ExperimentalModule,
|
||||||
|
ExportModule,
|
||||||
|
ImportModule,
|
||||||
InfoModule,
|
InfoModule,
|
||||||
OrderModule,
|
OrderModule,
|
||||||
PortfolioModule,
|
PortfolioModule,
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||||
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
||||||
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
|
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
|
||||||
@ -13,9 +15,10 @@ import { ExperimentalController } from './experimental.controller';
|
|||||||
import { ExperimentalService } from './experimental.service';
|
import { ExperimentalService } from './experimental.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
imports: [RedisCacheModule],
|
||||||
controllers: [ExperimentalController],
|
controllers: [ExperimentalController],
|
||||||
providers: [
|
providers: [
|
||||||
|
AccountService,
|
||||||
AlphaVantageService,
|
AlphaVantageService,
|
||||||
ConfigurationService,
|
ConfigurationService,
|
||||||
DataProviderService,
|
DataProviderService,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||||
import { Portfolio } from '@ghostfolio/api/models/portfolio';
|
import { Portfolio } from '@ghostfolio/api/models/portfolio';
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
@ -14,6 +15,7 @@ import { Data } from './interfaces/data.interface';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class ExperimentalService {
|
export class ExperimentalService {
|
||||||
public constructor(
|
public constructor(
|
||||||
|
private readonly accountService: AccountService,
|
||||||
private readonly dataProviderService: DataProviderService,
|
private readonly dataProviderService: DataProviderService,
|
||||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||||
private prisma: PrismaService,
|
private prisma: PrismaService,
|
||||||
@ -52,6 +54,7 @@ export class ExperimentalService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const portfolio = new Portfolio(
|
const portfolio = new Portfolio(
|
||||||
|
this.accountService,
|
||||||
this.dataProviderService,
|
this.dataProviderService,
|
||||||
this.exchangeRateDataService,
|
this.exchangeRateDataService,
|
||||||
this.rulesService
|
this.rulesService
|
||||||
|
23
apps/api/src/app/export/export.controller.ts
Normal file
23
apps/api/src/app/export/export.controller.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { Export } from '@ghostfolio/common/interfaces';
|
||||||
|
import { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
|
import { Controller, Get, Inject, UseGuards } from '@nestjs/common';
|
||||||
|
import { REQUEST } from '@nestjs/core';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
|
||||||
|
import { ExportService } from './export.service';
|
||||||
|
|
||||||
|
@Controller('export')
|
||||||
|
export class ExportController {
|
||||||
|
public constructor(
|
||||||
|
private readonly exportService: ExportService,
|
||||||
|
@Inject(REQUEST) private readonly request: RequestWithUser
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
public async export(): Promise<Export> {
|
||||||
|
return await this.exportService.export({
|
||||||
|
userId: this.request.user.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
32
apps/api/src/app/export/export.module.ts
Normal file
32
apps/api/src/app/export/export.module.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
|
||||||
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||||
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
||||||
|
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
|
||||||
|
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
||||||
|
import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
|
||||||
|
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
|
||||||
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { ExportController } from './export.controller';
|
||||||
|
import { ExportService } from './export.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [RedisCacheModule],
|
||||||
|
controllers: [ExportController],
|
||||||
|
providers: [
|
||||||
|
AlphaVantageService,
|
||||||
|
CacheService,
|
||||||
|
ConfigurationService,
|
||||||
|
DataGatheringService,
|
||||||
|
DataProviderService,
|
||||||
|
ExportService,
|
||||||
|
GhostfolioScraperApiService,
|
||||||
|
PrismaService,
|
||||||
|
RakutenRapidApiService,
|
||||||
|
YahooFinanceService
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class ExportModule {}
|
31
apps/api/src/app/export/export.service.ts
Normal file
31
apps/api/src/app/export/export.service.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { environment } from '@ghostfolio/api/environments/environment';
|
||||||
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
|
import { Export } from '@ghostfolio/common/interfaces';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ExportService {
|
||||||
|
public constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
|
public async export({ userId }: { userId: string }): Promise<Export> {
|
||||||
|
const orders = await this.prisma.order.findMany({
|
||||||
|
orderBy: { date: 'desc' },
|
||||||
|
select: {
|
||||||
|
currency: true,
|
||||||
|
dataSource: true,
|
||||||
|
date: true,
|
||||||
|
fee: true,
|
||||||
|
quantity: true,
|
||||||
|
symbol: true,
|
||||||
|
type: true,
|
||||||
|
unitPrice: true
|
||||||
|
},
|
||||||
|
where: { userId }
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
meta: { date: new Date().toISOString(), version: environment.version },
|
||||||
|
orders
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
7
apps/api/src/app/import/import-data.dto.ts
Normal file
7
apps/api/src/app/import/import-data.dto.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { Order } from '@prisma/client';
|
||||||
|
import { IsArray } from 'class-validator';
|
||||||
|
|
||||||
|
export class ImportDataDto {
|
||||||
|
@IsArray()
|
||||||
|
orders: Partial<Order>[];
|
||||||
|
}
|
50
apps/api/src/app/import/import.controller.ts
Normal file
50
apps/api/src/app/import/import.controller.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
|
import { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
HttpException,
|
||||||
|
Inject,
|
||||||
|
Post,
|
||||||
|
UseGuards
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { REQUEST } from '@nestjs/core';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
|
||||||
|
import { ImportDataDto } from './import-data.dto';
|
||||||
|
import { ImportService } from './import.service';
|
||||||
|
|
||||||
|
@Controller('import')
|
||||||
|
export class ImportController {
|
||||||
|
public constructor(
|
||||||
|
private readonly configurationService: ConfigurationService,
|
||||||
|
private readonly importService: ImportService,
|
||||||
|
@Inject(REQUEST) private readonly request: RequestWithUser
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
public async import(@Body() importData: ImportDataDto): Promise<void> {
|
||||||
|
if (!this.configurationService.get('ENABLE_FEATURE_IMPORT')) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
|
StatusCodes.FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await this.importService.import({
|
||||||
|
orders: importData.orders,
|
||||||
|
userId: this.request.user.id
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.BAD_REQUEST),
|
||||||
|
StatusCodes.BAD_REQUEST
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
apps/api/src/app/import/import.module.ts
Normal file
34
apps/api/src/app/import/import.module.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
|
||||||
|
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
||||||
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||||
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
||||||
|
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
|
||||||
|
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
||||||
|
import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
|
||||||
|
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
|
||||||
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { ImportController } from './import.controller';
|
||||||
|
import { ImportService } from './import.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [RedisCacheModule],
|
||||||
|
controllers: [ImportController],
|
||||||
|
providers: [
|
||||||
|
AlphaVantageService,
|
||||||
|
CacheService,
|
||||||
|
ConfigurationService,
|
||||||
|
DataGatheringService,
|
||||||
|
DataProviderService,
|
||||||
|
GhostfolioScraperApiService,
|
||||||
|
ImportService,
|
||||||
|
OrderService,
|
||||||
|
PrismaService,
|
||||||
|
RakutenRapidApiService,
|
||||||
|
YahooFinanceService
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class ImportModule {}
|
43
apps/api/src/app/import/import.service.ts
Normal file
43
apps/api/src/app/import/import.service.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Order } from '@prisma/client';
|
||||||
|
import { parseISO } from 'date-fns';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ImportService {
|
||||||
|
public constructor(private readonly orderService: OrderService) {}
|
||||||
|
|
||||||
|
public async import({
|
||||||
|
orders,
|
||||||
|
userId
|
||||||
|
}: {
|
||||||
|
orders: Partial<Order>[];
|
||||||
|
userId: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
for (const {
|
||||||
|
currency,
|
||||||
|
dataSource,
|
||||||
|
date,
|
||||||
|
fee,
|
||||||
|
quantity,
|
||||||
|
symbol,
|
||||||
|
type,
|
||||||
|
unitPrice
|
||||||
|
} of orders) {
|
||||||
|
await this.orderService.createOrder(
|
||||||
|
{
|
||||||
|
currency,
|
||||||
|
dataSource,
|
||||||
|
fee,
|
||||||
|
quantity,
|
||||||
|
symbol,
|
||||||
|
type,
|
||||||
|
unitPrice,
|
||||||
|
date: parseISO(<string>(<unknown>date)),
|
||||||
|
User: { connect: { id: userId } }
|
||||||
|
},
|
||||||
|
userId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -20,6 +20,7 @@ export class InfoService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async get(): Promise<InfoItem> {
|
public async get(): Promise<InfoItem> {
|
||||||
|
const info: Partial<InfoItem> = {};
|
||||||
const platforms = await this.prisma.platform.findMany({
|
const platforms = await this.prisma.platform.findMany({
|
||||||
orderBy: { name: 'asc' },
|
orderBy: { name: 'asc' },
|
||||||
select: { id: true, name: true }
|
select: { id: true, name: true }
|
||||||
@ -27,6 +28,10 @@ export class InfoService {
|
|||||||
|
|
||||||
const globalPermissions: string[] = [];
|
const globalPermissions: string[] = [];
|
||||||
|
|
||||||
|
if (this.configurationService.get('ENABLE_FEATURE_IMPORT')) {
|
||||||
|
globalPermissions.push(permissions.enableImport);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.configurationService.get('ENABLE_FEATURE_SOCIAL_LOGIN')) {
|
if (this.configurationService.get('ENABLE_FEATURE_SOCIAL_LOGIN')) {
|
||||||
globalPermissions.push(permissions.enableSocialLogin);
|
globalPermissions.push(permissions.enableSocialLogin);
|
||||||
}
|
}
|
||||||
@ -37,9 +42,12 @@ export class InfoService {
|
|||||||
|
|
||||||
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
||||||
globalPermissions.push(permissions.enableSubscription);
|
globalPermissions.push(permissions.enableSubscription);
|
||||||
|
|
||||||
|
info.stripePublicKey = this.configurationService.get('STRIPE_PUBLIC_KEY');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
...info,
|
||||||
globalPermissions,
|
globalPermissions,
|
||||||
platforms,
|
platforms,
|
||||||
currencies: Object.values(Currency),
|
currencies: Object.values(Currency),
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Currency, DataSource, Type } from '@prisma/client';
|
import { Currency, DataSource, Type } from '@prisma/client';
|
||||||
import { IsISO8601, IsNumber, IsString, ValidateIf } from 'class-validator';
|
import { IsISO8601, IsNumber, IsString } from 'class-validator';
|
||||||
|
|
||||||
export class CreateOrderDto {
|
export class CreateOrderDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
|
@ -142,10 +142,11 @@ export class PortfolioController {
|
|||||||
): Promise<{ [symbol: string]: PortfolioPosition }> {
|
): Promise<{ [symbol: string]: PortfolioPosition }> {
|
||||||
let details: { [symbol: string]: PortfolioPosition } = {};
|
let details: { [symbol: string]: PortfolioPosition } = {};
|
||||||
|
|
||||||
const impersonationUserId = await this.impersonationService.validateImpersonationId(
|
const impersonationUserId =
|
||||||
impersonationId,
|
await this.impersonationService.validateImpersonationId(
|
||||||
this.request.user.id
|
impersonationId,
|
||||||
);
|
this.request.user.id
|
||||||
|
);
|
||||||
|
|
||||||
const portfolio = await this.portfolioService.createPortfolio(
|
const portfolio = await this.portfolioService.createPortfolio(
|
||||||
impersonationUserId || this.request.user.id
|
impersonationUserId || this.request.user.id
|
||||||
@ -221,6 +222,7 @@ export class PortfolioController {
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
overview = nullifyValuesInObject(overview, [
|
overview = nullifyValuesInObject(overview, [
|
||||||
|
'cash',
|
||||||
'committedFunds',
|
'committedFunds',
|
||||||
'fees',
|
'fees',
|
||||||
'totalBuy',
|
'totalBuy',
|
||||||
@ -238,10 +240,11 @@ export class PortfolioController {
|
|||||||
@Query('range') range,
|
@Query('range') range,
|
||||||
@Res() res: Response
|
@Res() res: Response
|
||||||
): Promise<PortfolioPerformance> {
|
): Promise<PortfolioPerformance> {
|
||||||
const impersonationUserId = await this.impersonationService.validateImpersonationId(
|
const impersonationUserId =
|
||||||
impersonationId,
|
await this.impersonationService.validateImpersonationId(
|
||||||
this.request.user.id
|
impersonationId,
|
||||||
);
|
this.request.user.id
|
||||||
|
);
|
||||||
|
|
||||||
const portfolio = await this.portfolioService.createPortfolio(
|
const portfolio = await this.portfolioService.createPortfolio(
|
||||||
impersonationUserId || this.request.user.id
|
impersonationUserId || this.request.user.id
|
||||||
@ -306,10 +309,11 @@ export class PortfolioController {
|
|||||||
public async getReport(
|
public async getReport(
|
||||||
@Headers('impersonation-id') impersonationId
|
@Headers('impersonation-id') impersonationId
|
||||||
): Promise<PortfolioReport> {
|
): Promise<PortfolioReport> {
|
||||||
const impersonationUserId = await this.impersonationService.validateImpersonationId(
|
const impersonationUserId =
|
||||||
impersonationId,
|
await this.impersonationService.validateImpersonationId(
|
||||||
this.request.user.id
|
impersonationId,
|
||||||
);
|
this.request.user.id
|
||||||
|
);
|
||||||
|
|
||||||
const portfolio = await this.portfolioService.createPortfolio(
|
const portfolio = await this.portfolioService.createPortfolio(
|
||||||
impersonationUserId || this.request.user.id
|
impersonationUserId || this.request.user.id
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
|
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||||
|
import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
|
||||||
|
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
||||||
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
|
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
||||||
@ -11,10 +16,6 @@ import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
|||||||
import { RulesService } from '@ghostfolio/api/services/rules.service';
|
import { RulesService } from '@ghostfolio/api/services/rules.service';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { CacheService } from '../cache/cache.service';
|
|
||||||
import { OrderService } from '../order/order.service';
|
|
||||||
import { RedisCacheModule } from '../redis-cache/redis-cache.module';
|
|
||||||
import { UserService } from '../user/user.service';
|
|
||||||
import { PortfolioController } from './portfolio.controller';
|
import { PortfolioController } from './portfolio.controller';
|
||||||
import { PortfolioService } from './portfolio.service';
|
import { PortfolioService } from './portfolio.service';
|
||||||
|
|
||||||
@ -22,6 +23,7 @@ import { PortfolioService } from './portfolio.service';
|
|||||||
imports: [RedisCacheModule],
|
imports: [RedisCacheModule],
|
||||||
controllers: [PortfolioController],
|
controllers: [PortfolioController],
|
||||||
providers: [
|
providers: [
|
||||||
|
AccountService,
|
||||||
AlphaVantageService,
|
AlphaVantageService,
|
||||||
CacheService,
|
CacheService,
|
||||||
ConfigurationService,
|
ConfigurationService,
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||||
|
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
||||||
|
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
||||||
|
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||||
import { Portfolio } from '@ghostfolio/api/models/portfolio';
|
import { Portfolio } from '@ghostfolio/api/models/portfolio';
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
@ -14,6 +18,7 @@ import { REQUEST } from '@nestjs/core';
|
|||||||
import { DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
import {
|
import {
|
||||||
add,
|
add,
|
||||||
|
addMonths,
|
||||||
endOfToday,
|
endOfToday,
|
||||||
format,
|
format,
|
||||||
getDate,
|
getDate,
|
||||||
@ -30,9 +35,6 @@ import {
|
|||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
import * as roundTo from 'round-to';
|
import * as roundTo from 'round-to';
|
||||||
|
|
||||||
import { OrderService } from '../order/order.service';
|
|
||||||
import { RedisCacheService } from '../redis-cache/redis-cache.service';
|
|
||||||
import { UserService } from '../user/user.service';
|
|
||||||
import {
|
import {
|
||||||
HistoricalDataItem,
|
HistoricalDataItem,
|
||||||
PortfolioPositionDetail
|
PortfolioPositionDetail
|
||||||
@ -41,6 +43,7 @@ import {
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class PortfolioService {
|
export class PortfolioService {
|
||||||
public constructor(
|
public constructor(
|
||||||
|
private readonly accountService: AccountService,
|
||||||
private readonly dataProviderService: DataProviderService,
|
private readonly dataProviderService: DataProviderService,
|
||||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||||
private readonly impersonationService: ImpersonationService,
|
private readonly impersonationService: ImpersonationService,
|
||||||
@ -68,6 +71,7 @@ export class PortfolioService {
|
|||||||
JSON.parse(stringifiedPortfolio);
|
JSON.parse(stringifiedPortfolio);
|
||||||
|
|
||||||
portfolio = new Portfolio(
|
portfolio = new Portfolio(
|
||||||
|
this.accountService,
|
||||||
this.dataProviderService,
|
this.dataProviderService,
|
||||||
this.exchangeRateDataService,
|
this.exchangeRateDataService,
|
||||||
this.rulesService
|
this.rulesService
|
||||||
@ -84,6 +88,7 @@ export class PortfolioService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
portfolio = new Portfolio(
|
portfolio = new Portfolio(
|
||||||
|
this.accountService,
|
||||||
this.dataProviderService,
|
this.dataProviderService,
|
||||||
this.exchangeRateDataService,
|
this.exchangeRateDataService,
|
||||||
this.rulesService
|
this.rulesService
|
||||||
@ -192,12 +197,17 @@ export class PortfolioService {
|
|||||||
impersonationUserId || this.request.user.id
|
impersonationUserId || this.request.user.id
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { balance } = await this.accountService.getCashDetails(
|
||||||
|
impersonationUserId || this.request.user.id,
|
||||||
|
this.request.user.Settings.currency
|
||||||
|
);
|
||||||
const committedFunds = portfolio.getCommittedFunds();
|
const committedFunds = portfolio.getCommittedFunds();
|
||||||
const fees = portfolio.getFees();
|
const fees = portfolio.getFees();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
committedFunds,
|
committedFunds,
|
||||||
fees,
|
fees,
|
||||||
|
cash: balance,
|
||||||
ordersCount: portfolio.getOrders().length,
|
ordersCount: portfolio.getOrders().length,
|
||||||
totalBuy: portfolio.getTotalBuy(),
|
totalBuy: portfolio.getTotalBuy(),
|
||||||
totalSell: portfolio.getTotalSell()
|
totalSell: portfolio.getTotalSell()
|
||||||
@ -218,19 +228,18 @@ export class PortfolioService {
|
|||||||
impersonationUserId || this.request.user.id
|
impersonationUserId || this.request.user.id
|
||||||
);
|
);
|
||||||
|
|
||||||
const positions = portfolio.getPositions(new Date())[aSymbol];
|
const position = portfolio.getPositions(new Date())[aSymbol];
|
||||||
|
|
||||||
if (positions) {
|
if (position) {
|
||||||
let {
|
const {
|
||||||
averagePrice,
|
averagePrice,
|
||||||
currency,
|
currency,
|
||||||
firstBuyDate,
|
firstBuyDate,
|
||||||
investment,
|
investment,
|
||||||
marketPrice,
|
|
||||||
quantity,
|
quantity,
|
||||||
transactionCount
|
transactionCount
|
||||||
} = portfolio.getPositions(new Date())[aSymbol];
|
} = position;
|
||||||
|
let marketPrice = position.marketPrice;
|
||||||
const orders = portfolio.getOrders(aSymbol);
|
const orders = portfolio.getOrders(aSymbol);
|
||||||
|
|
||||||
const historicalData = await this.dataProviderService.getHistorical(
|
const historicalData = await this.dataProviderService.getHistorical(
|
||||||
@ -258,13 +267,14 @@ export class PortfolioService {
|
|||||||
isSameDay(currentDate, parseISO(orders[0]?.getDate())) ||
|
isSameDay(currentDate, parseISO(orders[0]?.getDate())) ||
|
||||||
isAfter(currentDate, parseISO(orders[0]?.getDate()))
|
isAfter(currentDate, parseISO(orders[0]?.getDate()))
|
||||||
) {
|
) {
|
||||||
// Get snapshot of first day of month
|
// Get snapshot of first day of next month
|
||||||
const snapshot = portfolio.get(setDate(currentDate, 1))[0]
|
const snapshot = portfolio.get(
|
||||||
.positions[aSymbol];
|
addMonths(setDate(currentDate, 1), 1)
|
||||||
|
)?.[0]?.positions[aSymbol];
|
||||||
orders.shift();
|
orders.shift();
|
||||||
|
|
||||||
if (snapshot?.averagePrice) {
|
if (snapshot?.averagePrice) {
|
||||||
currentAveragePrice = snapshot?.averagePrice;
|
currentAveragePrice = snapshot.averagePrice;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -343,13 +353,13 @@ export class PortfolioService {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
averagePrice: undefined,
|
averagePrice: undefined,
|
||||||
currency: currentData[aSymbol].currency,
|
currency: currentData[aSymbol]?.currency,
|
||||||
firstBuyDate: undefined,
|
firstBuyDate: undefined,
|
||||||
grossPerformance: undefined,
|
grossPerformance: undefined,
|
||||||
grossPerformancePercent: undefined,
|
grossPerformancePercent: undefined,
|
||||||
historicalData: historicalDataArray,
|
historicalData: historicalDataArray,
|
||||||
investment: undefined,
|
investment: undefined,
|
||||||
marketPrice: currentData[aSymbol].marketPrice,
|
marketPrice: currentData[aSymbol]?.marketPrice,
|
||||||
maxPrice: undefined,
|
maxPrice: undefined,
|
||||||
minPrice: undefined,
|
minPrice: undefined,
|
||||||
quantity: undefined,
|
quantity: undefined,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
export const environment = {
|
export const environment = {
|
||||||
production: true
|
production: true,
|
||||||
|
version: `v${require('../../../../package.json').version}`
|
||||||
};
|
};
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
export const environment = {
|
export const environment = {
|
||||||
production: false
|
production: false,
|
||||||
|
version: 'dev'
|
||||||
};
|
};
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||||
import { UNKNOWN_KEY, baseCurrency } from '@ghostfolio/common/config';
|
import { UNKNOWN_KEY, baseCurrency } from '@ghostfolio/common/config';
|
||||||
import { getUtc, getYesterday } from '@ghostfolio/common/helper';
|
import { getUtc, getYesterday } from '@ghostfolio/common/helper';
|
||||||
import {
|
import {
|
||||||
@ -16,6 +17,16 @@ import { MarketState } from '../services/interfaces/interfaces';
|
|||||||
import { RulesService } from '../services/rules.service';
|
import { RulesService } from '../services/rules.service';
|
||||||
import { Portfolio } from './portfolio';
|
import { Portfolio } from './portfolio';
|
||||||
|
|
||||||
|
jest.mock('../app/account/account.service', () => {
|
||||||
|
return {
|
||||||
|
AccountService: jest.fn().mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
getCashDetails: () => Promise.resolve({ accounts: [], balance: 0 })
|
||||||
|
};
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
jest.mock('../services/data-provider.service', () => {
|
jest.mock('../services/data-provider.service', () => {
|
||||||
return {
|
return {
|
||||||
DataProviderService: jest.fn().mockImplementation(() => {
|
DataProviderService: jest.fn().mockImplementation(() => {
|
||||||
@ -81,12 +92,14 @@ const DEFAULT_ACCOUNT_ID = '693a834b-eb89-42c9-ae47-35196c25d269';
|
|||||||
const USER_ID = 'ca6ce867-5d31-495a-bce9-5942bbca9237';
|
const USER_ID = 'ca6ce867-5d31-495a-bce9-5942bbca9237';
|
||||||
|
|
||||||
describe('Portfolio', () => {
|
describe('Portfolio', () => {
|
||||||
|
let accountService: AccountService;
|
||||||
let dataProviderService: DataProviderService;
|
let dataProviderService: DataProviderService;
|
||||||
let exchangeRateDataService: ExchangeRateDataService;
|
let exchangeRateDataService: ExchangeRateDataService;
|
||||||
let portfolio: Portfolio;
|
let portfolio: Portfolio;
|
||||||
let rulesService: RulesService;
|
let rulesService: RulesService;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
accountService = new AccountService(null, null, null);
|
||||||
dataProviderService = new DataProviderService(
|
dataProviderService = new DataProviderService(
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
@ -101,6 +114,7 @@ describe('Portfolio', () => {
|
|||||||
await exchangeRateDataService.initialize();
|
await exchangeRateDataService.initialize();
|
||||||
|
|
||||||
portfolio = new Portfolio(
|
portfolio = new Portfolio(
|
||||||
|
accountService,
|
||||||
dataProviderService,
|
dataProviderService,
|
||||||
exchangeRateDataService,
|
exchangeRateDataService,
|
||||||
rulesService
|
rulesService
|
||||||
@ -110,7 +124,9 @@ describe('Portfolio', () => {
|
|||||||
Account: [
|
Account: [
|
||||||
{
|
{
|
||||||
accountType: AccountType.SECURITIES,
|
accountType: AccountType.SECURITIES,
|
||||||
|
balance: 0,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
|
currency: Currency.USD,
|
||||||
id: DEFAULT_ACCOUNT_ID,
|
id: DEFAULT_ACCOUNT_ID,
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
name: 'Default Account',
|
name: 'Default Account',
|
||||||
@ -145,12 +161,52 @@ describe('Portfolio', () => {
|
|||||||
|
|
||||||
it('should return empty details', async () => {
|
it('should return empty details', async () => {
|
||||||
const details = await portfolio.getDetails('1d');
|
const details = await portfolio.getDetails('1d');
|
||||||
expect(details).toEqual({});
|
expect(details).toMatchObject({
|
||||||
|
_GF_CASH: {
|
||||||
|
accounts: {},
|
||||||
|
allocationCurrent: NaN, // TODO
|
||||||
|
allocationInvestment: NaN, // TODO
|
||||||
|
countries: [],
|
||||||
|
currency: 'CHF',
|
||||||
|
grossPerformance: 0,
|
||||||
|
grossPerformancePercent: 0,
|
||||||
|
investment: 0,
|
||||||
|
marketPrice: 0,
|
||||||
|
marketState: 'open',
|
||||||
|
name: 'Cash',
|
||||||
|
quantity: 0,
|
||||||
|
sectors: [],
|
||||||
|
symbol: '_GF_CASH',
|
||||||
|
transactionCount: 0,
|
||||||
|
type: 'Cash',
|
||||||
|
value: 0
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return empty details', async () => {
|
it('should return empty details', async () => {
|
||||||
const details = await portfolio.getDetails('max');
|
const details = await portfolio.getDetails('max');
|
||||||
expect(details).toEqual({});
|
expect(details).toMatchObject({
|
||||||
|
_GF_CASH: {
|
||||||
|
accounts: {},
|
||||||
|
allocationCurrent: NaN, // TODO
|
||||||
|
allocationInvestment: NaN, // TODO
|
||||||
|
countries: [],
|
||||||
|
currency: 'CHF',
|
||||||
|
grossPerformance: 0,
|
||||||
|
grossPerformancePercent: 0,
|
||||||
|
investment: 0,
|
||||||
|
marketPrice: 0,
|
||||||
|
marketState: 'open',
|
||||||
|
name: 'Cash',
|
||||||
|
quantity: 0,
|
||||||
|
sectors: [],
|
||||||
|
symbol: '_GF_CASH',
|
||||||
|
transactionCount: 0,
|
||||||
|
type: 'Cash',
|
||||||
|
value: 0
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return zero performance for 1d', async () => {
|
it('should return zero performance for 1d', async () => {
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||||
|
import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details.interface';
|
||||||
|
import { UNKNOWN_KEY, ghostfolioCashSymbol } from '@ghostfolio/common/config';
|
||||||
import { getToday, getYesterday, resetHours } from '@ghostfolio/common/helper';
|
import { getToday, getYesterday, resetHours } from '@ghostfolio/common/helper';
|
||||||
import {
|
import {
|
||||||
PortfolioItem,
|
PortfolioItem,
|
||||||
@ -11,7 +13,7 @@ import {
|
|||||||
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
||||||
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
||||||
import { DateRange, OrderWithAccount } from '@ghostfolio/common/types';
|
import { DateRange, OrderWithAccount } from '@ghostfolio/common/types';
|
||||||
import { Prisma } from '@prisma/client';
|
import { Currency, Prisma } from '@prisma/client';
|
||||||
import { continents, countries } from 'countries-list';
|
import { continents, countries } from 'countries-list';
|
||||||
import {
|
import {
|
||||||
add,
|
add,
|
||||||
@ -34,7 +36,7 @@ import * as roundTo from 'round-to';
|
|||||||
|
|
||||||
import { DataProviderService } from '../services/data-provider.service';
|
import { DataProviderService } from '../services/data-provider.service';
|
||||||
import { ExchangeRateDataService } from '../services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '../services/exchange-rate-data.service';
|
||||||
import { IOrder } from '../services/interfaces/interfaces';
|
import { IOrder, MarketState, Type } from '../services/interfaces/interfaces';
|
||||||
import { RulesService } from '../services/rules.service';
|
import { RulesService } from '../services/rules.service';
|
||||||
import { PortfolioInterface } from './interfaces/portfolio.interface';
|
import { PortfolioInterface } from './interfaces/portfolio.interface';
|
||||||
import { Order } from './order';
|
import { Order } from './order';
|
||||||
@ -54,6 +56,7 @@ export class Portfolio implements PortfolioInterface {
|
|||||||
private user: UserWithSettings;
|
private user: UserWithSettings;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
|
private accountService: AccountService,
|
||||||
private dataProviderService: DataProviderService,
|
private dataProviderService: DataProviderService,
|
||||||
private exchangeRateDataService: ExchangeRateDataService,
|
private exchangeRateDataService: ExchangeRateDataService,
|
||||||
private rulesService: RulesService
|
private rulesService: RulesService
|
||||||
@ -232,10 +235,14 @@ export class Portfolio implements PortfolioInterface {
|
|||||||
|
|
||||||
const [portfolioItemsNow] = await this.get(new Date());
|
const [portfolioItemsNow] = await this.get(new Date());
|
||||||
|
|
||||||
const investment = this.getInvestment(new Date());
|
const cashDetails = await this.accountService.getCashDetails(
|
||||||
|
this.user.id,
|
||||||
|
this.user.Settings.currency
|
||||||
|
);
|
||||||
|
const investment = this.getInvestment(new Date()) + cashDetails.balance;
|
||||||
const portfolioItems = this.get(new Date());
|
const portfolioItems = this.get(new Date());
|
||||||
const symbols = this.getSymbols(new Date());
|
const symbols = this.getSymbols(new Date());
|
||||||
const value = this.getValue();
|
const value = this.getValue() + cashDetails.balance;
|
||||||
|
|
||||||
const details: { [symbol: string]: PortfolioPosition } = {};
|
const details: { [symbol: string]: PortfolioPosition } = {};
|
||||||
|
|
||||||
@ -372,6 +379,12 @@ export class Portfolio implements PortfolioInterface {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
details[ghostfolioCashSymbol] = await this.getCashPosition({
|
||||||
|
cashDetails,
|
||||||
|
investment,
|
||||||
|
value
|
||||||
|
});
|
||||||
|
|
||||||
return details;
|
return details;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -644,6 +657,46 @@ export class Portfolio implements PortfolioInterface {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getCashPosition({
|
||||||
|
cashDetails,
|
||||||
|
investment,
|
||||||
|
value
|
||||||
|
}: {
|
||||||
|
cashDetails: CashDetails;
|
||||||
|
investment: number;
|
||||||
|
value: number;
|
||||||
|
}) {
|
||||||
|
const accounts = {};
|
||||||
|
const cashValue = cashDetails.balance;
|
||||||
|
|
||||||
|
cashDetails.accounts.forEach((account) => {
|
||||||
|
accounts[account.name] = {
|
||||||
|
current: account.balance,
|
||||||
|
original: account.balance
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
accounts,
|
||||||
|
allocationCurrent: cashValue / value,
|
||||||
|
allocationInvestment: cashValue / investment,
|
||||||
|
countries: [],
|
||||||
|
currency: Currency.CHF,
|
||||||
|
grossPerformance: 0,
|
||||||
|
grossPerformancePercent: 0,
|
||||||
|
investment: cashValue,
|
||||||
|
marketPrice: 0,
|
||||||
|
marketState: MarketState.open,
|
||||||
|
name: Type.Cash,
|
||||||
|
quantity: 0,
|
||||||
|
sectors: [],
|
||||||
|
symbol: ghostfolioCashSymbol,
|
||||||
|
type: Type.Cash,
|
||||||
|
transactionCount: 0,
|
||||||
|
value: cashValue
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: Refactor
|
* TODO: Refactor
|
||||||
*/
|
*/
|
||||||
|
@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
import { bool, cleanEnv, host, json, num, port, str } from 'envalid';
|
import { bool, cleanEnv, host, json, num, port, str } from 'envalid';
|
||||||
|
|
||||||
|
import { environment } from '../environments/environment';
|
||||||
import { Environment } from './interfaces/environment.interface';
|
import { Environment } from './interfaces/environment.interface';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -16,6 +17,7 @@ export class ConfigurationService {
|
|||||||
DATA_SOURCES: json({ default: JSON.stringify([DataSource.YAHOO]) }),
|
DATA_SOURCES: json({ default: JSON.stringify([DataSource.YAHOO]) }),
|
||||||
ENABLE_FEATURE_CUSTOM_SYMBOLS: bool({ default: false }),
|
ENABLE_FEATURE_CUSTOM_SYMBOLS: bool({ default: false }),
|
||||||
ENABLE_FEATURE_FEAR_AND_GREED_INDEX: bool({ default: false }),
|
ENABLE_FEATURE_FEAR_AND_GREED_INDEX: bool({ default: false }),
|
||||||
|
ENABLE_FEATURE_IMPORT: bool({ default: !environment.production }),
|
||||||
ENABLE_FEATURE_SOCIAL_LOGIN: bool({ default: false }),
|
ENABLE_FEATURE_SOCIAL_LOGIN: bool({ default: false }),
|
||||||
ENABLE_FEATURE_STATISTICS: bool({ default: false }),
|
ENABLE_FEATURE_STATISTICS: bool({ default: false }),
|
||||||
ENABLE_FEATURE_SUBSCRIPTION: bool({ default: false }),
|
ENABLE_FEATURE_SUBSCRIPTION: bool({ default: false }),
|
||||||
@ -28,6 +30,7 @@ export class ConfigurationService {
|
|||||||
REDIS_HOST: str({ default: 'localhost' }),
|
REDIS_HOST: str({ default: 'localhost' }),
|
||||||
REDIS_PORT: port({ default: 6379 }),
|
REDIS_PORT: port({ default: 6379 }),
|
||||||
ROOT_URL: str({ default: 'http://localhost:4200' }),
|
ROOT_URL: str({ default: 'http://localhost:4200' }),
|
||||||
|
STRIPE_PUBLIC_KEY: str({ default: '' }),
|
||||||
STRIPE_SECRET_KEY: str({ default: '' }),
|
STRIPE_SECRET_KEY: str({ default: '' }),
|
||||||
WEB_AUTH_RP_ID: host({ default: 'localhost' })
|
WEB_AUTH_RP_ID: host({ default: 'localhost' })
|
||||||
});
|
});
|
||||||
|
@ -7,6 +7,7 @@ export interface Environment extends CleanedEnvAccessors {
|
|||||||
DATA_SOURCES: string | string[]; // string is not correct, error in envalid?
|
DATA_SOURCES: string | string[]; // string is not correct, error in envalid?
|
||||||
ENABLE_FEATURE_CUSTOM_SYMBOLS: boolean;
|
ENABLE_FEATURE_CUSTOM_SYMBOLS: boolean;
|
||||||
ENABLE_FEATURE_FEAR_AND_GREED_INDEX: boolean;
|
ENABLE_FEATURE_FEAR_AND_GREED_INDEX: boolean;
|
||||||
|
ENABLE_FEATURE_IMPORT: boolean;
|
||||||
ENABLE_FEATURE_SOCIAL_LOGIN: boolean;
|
ENABLE_FEATURE_SOCIAL_LOGIN: boolean;
|
||||||
ENABLE_FEATURE_STATISTICS: boolean;
|
ENABLE_FEATURE_STATISTICS: boolean;
|
||||||
ENABLE_FEATURE_SUBSCRIPTION: boolean;
|
ENABLE_FEATURE_SUBSCRIPTION: boolean;
|
||||||
@ -19,6 +20,7 @@ export interface Environment extends CleanedEnvAccessors {
|
|||||||
REDIS_HOST: string;
|
REDIS_HOST: string;
|
||||||
REDIS_PORT: number;
|
REDIS_PORT: number;
|
||||||
ROOT_URL: string;
|
ROOT_URL: string;
|
||||||
|
STRIPE_PUBLIC_KEY: string;
|
||||||
STRIPE_SECRET_KEY: string;
|
STRIPE_SECRET_KEY: string;
|
||||||
WEB_AUTH_RP_ID: string;
|
WEB_AUTH_RP_ID: string;
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ export const MarketState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const Type = {
|
export const Type = {
|
||||||
|
Cash: 'Cash',
|
||||||
Cryptocurrency: 'Cryptocurrency',
|
Cryptocurrency: 'Cryptocurrency',
|
||||||
ETF: 'ETF',
|
ETF: 'ETF',
|
||||||
Stock: 'Stock',
|
Stock: 'Stock',
|
||||||
|
@ -5,13 +5,7 @@ module.exports = {
|
|||||||
globals: {
|
globals: {
|
||||||
'ts-jest': {
|
'ts-jest': {
|
||||||
tsconfig: '<rootDir>/tsconfig.spec.json',
|
tsconfig: '<rootDir>/tsconfig.spec.json',
|
||||||
stringifyContentPathRegex: '\\.(html|svg)$',
|
stringifyContentPathRegex: '\\.(html|svg)$'
|
||||||
astTransformers: {
|
|
||||||
before: [
|
|
||||||
'jest-preset-angular/build/InlineFilesTransformer',
|
|
||||||
'jest-preset-angular/build/StripStylesTransformer'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
coverageDirectory: '../../coverage/apps/client',
|
coverageDirectory: '../../coverage/apps/client',
|
||||||
@ -19,5 +13,6 @@ module.exports = {
|
|||||||
'jest-preset-angular/build/serializers/no-ng-attributes',
|
'jest-preset-angular/build/serializers/no-ng-attributes',
|
||||||
'jest-preset-angular/build/serializers/ng-snapshot',
|
'jest-preset-angular/build/serializers/ng-snapshot',
|
||||||
'jest-preset-angular/build/serializers/html-comment'
|
'jest-preset-angular/build/serializers/html-comment'
|
||||||
]
|
],
|
||||||
|
transform: { '^.+\\.(ts|js|html)$': 'jest-preset-angular' }
|
||||||
};
|
};
|
||||||
|
@ -7,10 +7,6 @@
|
|||||||
.create-account-box {
|
.create-account-box {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
|
|
||||||
.link {
|
|
||||||
color: rgba(var(--palette-primary-500), 1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,11 @@ import {
|
|||||||
OnInit
|
OnInit
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { NavigationEnd, PRIMARY_OUTLET, Router } from '@angular/router';
|
import { NavigationEnd, PRIMARY_OUTLET, Router } from '@angular/router';
|
||||||
import { primaryColorHex, secondaryColorHex } from '@ghostfolio/common/config';
|
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 { MaterialCssVarsService } from 'angular-material-css-vars';
|
import { MaterialCssVarsService } from 'angular-material-css-vars';
|
||||||
@ -52,10 +56,6 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
public ngOnInit() {
|
public ngOnInit() {
|
||||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||||
|
|
||||||
this.dataService.fetchInfo().subscribe((info) => {
|
|
||||||
this.info = info;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.router.events
|
this.router.events
|
||||||
.pipe(filter((event) => event instanceof NavigationEnd))
|
.pipe(filter((event) => event instanceof NavigationEnd))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
@ -63,6 +63,8 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
const urlSegmentGroup = urlTree.root.children[PRIMARY_OUTLET];
|
const urlSegmentGroup = urlTree.root.children[PRIMARY_OUTLET];
|
||||||
const urlSegments = urlSegmentGroup.segments;
|
const urlSegments = urlSegmentGroup.segments;
|
||||||
this.currentRoute = urlSegments[0].path;
|
this.currentRoute = urlSegments[0].path;
|
||||||
|
|
||||||
|
this.info = this.dataService.fetchInfo();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.userService.stateChanged
|
this.userService.stateChanged
|
||||||
@ -106,5 +108,6 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
this.materialCssVarsService.setPrimaryColor(primaryColorHex);
|
this.materialCssVarsService.setPrimaryColor(primaryColorHex);
|
||||||
this.materialCssVarsService.setAccentColor(secondaryColorHex);
|
this.materialCssVarsService.setAccentColor(secondaryColorHex);
|
||||||
|
this.materialCssVarsService.setWarnColor(warnColorHex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
|||||||
import { MaterialCssVarsModule } from 'angular-material-css-vars';
|
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 } from 'ngx-stripe';
|
import { NgxStripeModule, STRIPE_PUBLISHABLE_KEY } from 'ngx-stripe';
|
||||||
|
|
||||||
import { environment } from '../environments/environment';
|
import { environment } from '../environments/environment';
|
||||||
import { CustomDateAdapter } from './adapter/custom-date-adapter';
|
import { CustomDateAdapter } from './adapter/custom-date-adapter';
|
||||||
@ -27,6 +27,10 @@ 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';
|
||||||
|
|
||||||
|
export function NgxStripeFactory(): string {
|
||||||
|
return environment.stripePublicKey;
|
||||||
|
}
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [AppComponent],
|
declarations: [AppComponent],
|
||||||
imports: [
|
imports: [
|
||||||
@ -57,7 +61,11 @@ import { LanguageService } from './core/language.service';
|
|||||||
useClass: CustomDateAdapter,
|
useClass: CustomDateAdapter,
|
||||||
deps: [LanguageService, MAT_DATE_LOCALE, Platform]
|
deps: [LanguageService, MAT_DATE_LOCALE, Platform]
|
||||||
},
|
},
|
||||||
{ provide: MAT_DATE_FORMATS, useValue: DateFormats }
|
{ provide: MAT_DATE_FORMATS, useValue: DateFormats },
|
||||||
|
{
|
||||||
|
provide: STRIPE_PUBLISHABLE_KEY,
|
||||||
|
useFactory: NgxStripeFactory
|
||||||
|
}
|
||||||
],
|
],
|
||||||
bootstrap: [AppComponent]
|
bootstrap: [AppComponent]
|
||||||
})
|
})
|
||||||
|
@ -26,6 +26,27 @@
|
|||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="transactions">
|
||||||
|
<th *matHeaderCellDef class="text-right" i18n mat-header-cell>
|
||||||
|
Transactions
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="text-right" mat-cell>
|
||||||
|
{{ element.Order?.length }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="balance">
|
||||||
|
<th *matHeaderCellDef class="text-right" i18n mat-header-cell>Balance</th>
|
||||||
|
<td *matCellDef="let element" class="text-right" mat-cell>
|
||||||
|
<gf-value
|
||||||
|
class="d-inline-block justify-content-end"
|
||||||
|
[currency]="element.currency"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="element.balance"
|
||||||
|
></gf-value>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="actions">
|
<ng-container matColumnDef="actions">
|
||||||
<th *matHeaderCellDef class="px-1 text-center" i18n mat-header-cell></th>
|
<th *matHeaderCellDef class="px-1 text-center" i18n mat-header-cell></th>
|
||||||
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
||||||
@ -53,15 +74,6 @@
|
|||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="transactions">
|
|
||||||
<th *matHeaderCellDef class="text-right" i18n mat-header-cell>
|
|
||||||
Transactions
|
|
||||||
</th>
|
|
||||||
<td *matCellDef="let element" class="text-right" mat-cell>
|
|
||||||
{{ element.Order?.length }}
|
|
||||||
</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
||||||
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
|
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
|
||||||
</table>
|
</table>
|
||||||
|
@ -28,7 +28,8 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
@Output() accountDeleted = new EventEmitter<string>();
|
@Output() accountDeleted = new EventEmitter<string>();
|
||||||
@Output() accountToUpdate = new EventEmitter<AccountModel>();
|
@Output() accountToUpdate = new EventEmitter<AccountModel>();
|
||||||
|
|
||||||
public dataSource: MatTableDataSource<AccountModel> = new MatTableDataSource();
|
public dataSource: MatTableDataSource<AccountModel> =
|
||||||
|
new MatTableDataSource();
|
||||||
public displayedColumns = [];
|
public displayedColumns = [];
|
||||||
public isLoading = true;
|
public isLoading = true;
|
||||||
public routeQueryParams: Subscription;
|
public routeQueryParams: Subscription;
|
||||||
@ -40,7 +41,7 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
public ngOnInit() {}
|
public ngOnInit() {}
|
||||||
|
|
||||||
public ngOnChanges() {
|
public ngOnChanges() {
|
||||||
this.displayedColumns = ['account', 'platform', 'transactions'];
|
this.displayedColumns = ['account', 'platform', 'transactions', 'balance'];
|
||||||
|
|
||||||
if (this.showActions) {
|
if (this.showActions) {
|
||||||
this.displayedColumns.push('actions');
|
this.displayedColumns.push('actions');
|
||||||
|
@ -5,10 +5,7 @@
|
|||||||
z-index: 999;
|
z-index: 999;
|
||||||
|
|
||||||
.mat-toolbar {
|
.mat-toolbar {
|
||||||
background-color: rgba(
|
background-color: rgba(var(--light-disabled-text));
|
||||||
var(--light-primary-text),
|
|
||||||
var(--palette-foreground-disabled-alpha)
|
|
||||||
);
|
|
||||||
|
|
||||||
.spacer {
|
.spacer {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
@ -28,11 +25,6 @@
|
|||||||
|
|
||||||
:host-context(.is-dark-theme) {
|
:host-context(.is-dark-theme) {
|
||||||
.mat-toolbar {
|
.mat-toolbar {
|
||||||
background-color: rgba(
|
background-color: rgba(39, 39, 39, $alpha-disabled-text);
|
||||||
39,
|
|
||||||
39,
|
|
||||||
39,
|
|
||||||
var(--palette-foreground-disabled-alpha)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,6 +51,7 @@ export class HeaderComponent implements OnChanges {
|
|||||||
) {
|
) {
|
||||||
this.impersonationStorageService
|
this.impersonationStorageService
|
||||||
.onChangeHasImpersonation()
|
.onChangeHasImpersonation()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((id) => {
|
.subscribe((id) => {
|
||||||
this.impersonationId = id;
|
this.impersonationId = id;
|
||||||
});
|
});
|
||||||
@ -98,23 +99,26 @@ export class HeaderComponent implements OnChanges {
|
|||||||
width: '30rem'
|
width: '30rem'
|
||||||
});
|
});
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe((data) => {
|
dialogRef
|
||||||
if (data?.accessToken) {
|
.afterClosed()
|
||||||
this.dataService
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.loginAnonymous(data?.accessToken)
|
.subscribe((data) => {
|
||||||
.pipe(
|
if (data?.accessToken) {
|
||||||
catchError(() => {
|
this.dataService
|
||||||
alert('Oops! Incorrect Security Token.');
|
.loginAnonymous(data?.accessToken)
|
||||||
|
.pipe(
|
||||||
|
catchError(() => {
|
||||||
|
alert('Oops! Incorrect Security Token.');
|
||||||
|
|
||||||
return EMPTY;
|
return EMPTY;
|
||||||
}),
|
}),
|
||||||
takeUntil(this.unsubscribeSubject)
|
takeUntil(this.unsubscribeSubject)
|
||||||
)
|
)
|
||||||
.subscribe(({ authToken }) => {
|
.subscribe(({ authToken }) => {
|
||||||
this.setToken(authToken);
|
this.setToken(authToken);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public setToken(aToken: string) {
|
public setToken(aToken: string) {
|
||||||
@ -125,4 +129,9 @@ export class HeaderComponent implements OnChanges {
|
|||||||
|
|
||||||
this.router.navigate(['/']);
|
this.router.navigate(['/']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,8 @@ import { MatMenuModule } from '@angular/material/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';
|
||||||
|
import { GfLogoModule } from '@ghostfolio/client/components/logo/logo.module';
|
||||||
|
|
||||||
import { GfLogoModule } from '../logo/logo.module';
|
|
||||||
import { HeaderComponent } from './header.component';
|
import { HeaderComponent } from './header.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -2,6 +2,7 @@ import 'chartjs-adapter-date-fns';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
Input,
|
Input,
|
||||||
OnChanges,
|
OnChanges,
|
||||||
@ -44,7 +45,7 @@ export class LineChartComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
public chart: Chart;
|
public chart: Chart;
|
||||||
public isLoading = true;
|
public isLoading = true;
|
||||||
|
|
||||||
public constructor() {
|
public constructor(private changeDetectorRef: ChangeDetectorRef) {
|
||||||
Chart.register(
|
Chart.register(
|
||||||
Filler,
|
Filler,
|
||||||
LineController,
|
LineController,
|
||||||
@ -59,7 +60,12 @@ export class LineChartComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
|
|
||||||
public ngOnChanges() {
|
public ngOnChanges() {
|
||||||
if (this.historicalDataItems) {
|
if (this.historicalDataItems) {
|
||||||
this.initialize();
|
setTimeout(() => {
|
||||||
|
// Wait for the chartCanvas
|
||||||
|
this.initialize();
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,8 +85,6 @@ export class LineChartComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
marketPrices.push(historicalDataItem.value);
|
marketPrices.push(historicalDataItem.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
const canvas = document.getElementById('chartCanvas');
|
|
||||||
|
|
||||||
const gradient = this.chartCanvas?.nativeElement
|
const gradient = this.chartCanvas?.nativeElement
|
||||||
?.getContext('2d')
|
?.getContext('2d')
|
||||||
.createLinearGradient(
|
.createLinearGradient(
|
||||||
@ -89,11 +93,14 @@ export class LineChartComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
0,
|
0,
|
||||||
(this.chartCanvas.nativeElement.parentNode.offsetHeight * 4) / 5
|
(this.chartCanvas.nativeElement.parentNode.offsetHeight * 4) / 5
|
||||||
);
|
);
|
||||||
gradient.addColorStop(
|
|
||||||
0,
|
if (gradient) {
|
||||||
`rgba(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b}, 0.01)`
|
gradient.addColorStop(
|
||||||
);
|
0,
|
||||||
gradient.addColorStop(1, getBackgroundColor());
|
`rgba(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b}, 0.01)`
|
||||||
|
);
|
||||||
|
gradient.addColorStop(1, getBackgroundColor());
|
||||||
|
}
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
labels,
|
labels,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<span class="align-items-center d-flex"
|
<span class="align-items-center d-flex"
|
||||||
><span class="d-inline-block logo mr-1"></span>
|
><span class="d-inline-block logo mr-1"></span>
|
||||||
<span class="name">Ghostfolio</span></span
|
<span *ngIf="!hideName" class="name">Ghostfolio</span></span
|
||||||
>
|
>
|
||||||
|
@ -14,10 +14,12 @@ import {
|
|||||||
})
|
})
|
||||||
export class LogoComponent implements OnInit {
|
export class LogoComponent implements OnInit {
|
||||||
@HostBinding('class') @Input() size: 'large' | 'medium';
|
@HostBinding('class') @Input() size: 'large' | 'medium';
|
||||||
|
@Input() hideName: boolean;
|
||||||
|
|
||||||
public constructor() {}
|
public constructor() {}
|
||||||
|
|
||||||
public ngOnInit() {
|
public ngOnInit() {
|
||||||
this.size = this.size || 'medium';
|
this.hideName = this.hideName ?? false;
|
||||||
|
this.size = this.size ?? 'medium';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
<a
|
<div class="p-3">
|
||||||
class="align-items-center justify-content-center"
|
<div class="d-flex justify-content-center mb-1">
|
||||||
color="primary"
|
<gf-logo size="large" [hideName]="true"></gf-logo>
|
||||||
[routerLink]="['/transactions']"
|
</div>
|
||||||
mat-button
|
<a
|
||||||
>
|
class="align-items-center justify-content-center"
|
||||||
<ion-icon class="mr-1" name="time-outline" size="large"></ion-icon>
|
color="primary"
|
||||||
<span i18n>Time to add your first transaction.</span>
|
[routerLink]="['/transactions']"
|
||||||
</a>
|
mat-button
|
||||||
|
>
|
||||||
|
<span i18n>Time to add your first transaction.</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
@ -1,3 +1,13 @@
|
|||||||
:host {
|
:host {
|
||||||
|
border: 1px solid rgba(var(--dark-dividers));
|
||||||
|
border-radius: 0.25rem;
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
|
gf-logo {
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(.is-dark-theme) {
|
||||||
|
border-color: rgba(var(--light-dividers));
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,14 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { GfLogoModule } from '@ghostfolio/client/components/logo/logo.module';
|
||||||
|
|
||||||
import { NoTransactionsInfoComponent } from './no-transactions-info.component';
|
import { NoTransactionsInfoComponent } from './no-transactions-info.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [NoTransactionsInfoComponent],
|
declarations: [NoTransactionsInfoComponent],
|
||||||
exports: [NoTransactionsInfoComponent],
|
exports: [NoTransactionsInfoComponent],
|
||||||
imports: [CommonModule, MatButtonModule, RouterModule],
|
imports: [CommonModule, GfLogoModule, MatButtonModule, RouterModule],
|
||||||
providers: [],
|
providers: [],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
|
@ -7,6 +7,8 @@ import {
|
|||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { isToday, parse } from 'date-fns';
|
import { isToday, parse } from 'date-fns';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
import { LineChartItem } from '../line-chart/interfaces/line-chart.interface';
|
import { LineChartItem } from '../line-chart/interfaces/line-chart.interface';
|
||||||
import { PositionDetailDialogParams } from './interfaces/interfaces';
|
import { PositionDetailDialogParams } from './interfaces/interfaces';
|
||||||
@ -27,6 +29,8 @@ export class PerformanceChartDialog {
|
|||||||
public historicalDataItems: LineChartItem[];
|
public historicalDataItems: LineChartItem[];
|
||||||
public title: string;
|
public title: string;
|
||||||
|
|
||||||
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
@ -35,6 +39,7 @@ export class PerformanceChartDialog {
|
|||||||
) {
|
) {
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchPositionDetail(this.benchmarkSymbol)
|
.fetchPositionDetail(this.benchmarkSymbol)
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(({ currency, firstBuyDate, historicalData, marketPrice }) => {
|
.subscribe(({ currency, firstBuyDate, historicalData, marketPrice }) => {
|
||||||
this.benchmarkDataItems = [];
|
this.benchmarkDataItems = [];
|
||||||
this.currency = currency;
|
this.currency = currency;
|
||||||
@ -84,4 +89,9 @@ export class PerformanceChartDialog {
|
|||||||
public onClose(): void {
|
public onClose(): void {
|
||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,18 @@
|
|||||||
<div class="container p-0">
|
<div class="container p-0">
|
||||||
|
<div class="row px-3 py-1">
|
||||||
|
<div class="d-flex flex-grow-1" i18n>Cash</div>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<gf-value
|
||||||
|
class="justify-content-end"
|
||||||
|
[currency]="baseCurrency"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="isLoading ? undefined : overview?.cash"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col"><hr /></div>
|
||||||
|
</div>
|
||||||
<div class="row px-3 py-1">
|
<div class="row px-3 py-1">
|
||||||
<div class="d-flex flex-grow-1" i18n>Buy</div>
|
<div class="d-flex flex-grow-1" i18n>Buy</div>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||||
import { getCssVariable, getTextColor } from '@ghostfolio/common/helper';
|
import { getTextColor } from '@ghostfolio/common/helper';
|
||||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||||
import { Currency } from '@prisma/client';
|
import { Currency } from '@prisma/client';
|
||||||
import { Tooltip } from 'chart.js';
|
import { Tooltip } from 'chart.js';
|
||||||
@ -43,9 +43,7 @@ export class PortfolioProportionChartComponent
|
|||||||
private colorMap: {
|
private colorMap: {
|
||||||
[symbol: string]: string;
|
[symbol: string]: string;
|
||||||
} = {
|
} = {
|
||||||
[UNKNOWN_KEY]: `rgba(${getTextColor()}, ${getCssVariable(
|
[UNKNOWN_KEY]: `rgba(${getTextColor()}, 0.12)`
|
||||||
'--palette-foreground-divider-alpha'
|
|
||||||
)})`
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
|
@ -2,11 +2,14 @@ import {
|
|||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
Inject
|
Inject,
|
||||||
|
OnDestroy
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { format, isSameMonth, isToday, parseISO } from 'date-fns';
|
import { format, isSameMonth, isToday, parseISO } from 'date-fns';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
import { LineChartItem } from '../../line-chart/interfaces/line-chart.interface';
|
import { LineChartItem } from '../../line-chart/interfaces/line-chart.interface';
|
||||||
import { PositionDetailDialogParams } from './interfaces/interfaces';
|
import { PositionDetailDialogParams } from './interfaces/interfaces';
|
||||||
@ -18,7 +21,7 @@ import { PositionDetailDialogParams } from './interfaces/interfaces';
|
|||||||
templateUrl: 'position-detail-dialog.html',
|
templateUrl: 'position-detail-dialog.html',
|
||||||
styleUrls: ['./position-detail-dialog.component.scss']
|
styleUrls: ['./position-detail-dialog.component.scss']
|
||||||
})
|
})
|
||||||
export class PositionDetailDialog {
|
export class PositionDetailDialog implements OnDestroy {
|
||||||
public averagePrice: number;
|
public averagePrice: number;
|
||||||
public benchmarkDataItems: LineChartItem[];
|
public benchmarkDataItems: LineChartItem[];
|
||||||
public currency: string;
|
public currency: string;
|
||||||
@ -33,6 +36,8 @@ export class PositionDetailDialog {
|
|||||||
public quantity: number;
|
public quantity: number;
|
||||||
public transactionCount: number;
|
public transactionCount: number;
|
||||||
|
|
||||||
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
@ -41,6 +46,7 @@ export class PositionDetailDialog {
|
|||||||
) {
|
) {
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchPositionDetail(data.symbol)
|
.fetchPositionDetail(data.symbol)
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(
|
.subscribe(
|
||||||
({
|
({
|
||||||
averagePrice,
|
averagePrice,
|
||||||
@ -135,4 +141,9 @@ export class PositionDetailDialog {
|
|||||||
public onClose(): void {
|
public onClose(): void {
|
||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,8 +72,11 @@ export class PositionComponent implements OnDestroy, OnInit {
|
|||||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||||
});
|
});
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe(() => {
|
dialogRef
|
||||||
this.router.navigate(['.'], { relativeTo: this.route });
|
.afterClosed()
|
||||||
});
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.router.navigate(['.'], { relativeTo: this.route });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,7 +82,13 @@
|
|||||||
<tr
|
<tr
|
||||||
*matRowDef="let row; columns: displayedColumns"
|
*matRowDef="let row; columns: displayedColumns"
|
||||||
mat-row
|
mat-row
|
||||||
(click)="onOpenPositionDialog({ symbol: row.symbol, title: row.name })"
|
[ngClass]="{
|
||||||
|
'cursor-pointer': !this.ignoreTypes.includes(row.type)
|
||||||
|
}"
|
||||||
|
(click)="
|
||||||
|
!this.ignoreTypes.includes(row.type) &&
|
||||||
|
onOpenPositionDialog({ symbol: row.symbol, title: row.name })
|
||||||
|
"
|
||||||
></tr>
|
></tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
@ -19,7 +19,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mat-row {
|
.mat-row {
|
||||||
cursor: pointer;
|
&.cursor-pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import {
|
|||||||
EventEmitter,
|
EventEmitter,
|
||||||
Input,
|
Input,
|
||||||
OnChanges,
|
OnChanges,
|
||||||
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
Output,
|
Output,
|
||||||
ViewChild
|
ViewChild
|
||||||
@ -13,6 +14,7 @@ import { MatPaginator } from '@angular/material/paginator';
|
|||||||
import { MatSort } from '@angular/material/sort';
|
import { MatSort } from '@angular/material/sort';
|
||||||
import { MatTableDataSource } from '@angular/material/table';
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { Type } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||||
import { Order as OrderModel } from '@prisma/client';
|
import { Order as OrderModel } from '@prisma/client';
|
||||||
import { Subject, Subscription } from 'rxjs';
|
import { Subject, Subscription } from 'rxjs';
|
||||||
@ -26,7 +28,7 @@ import { PositionDetailDialog } from '../position/position-detail-dialog/positio
|
|||||||
templateUrl: './positions-table.component.html',
|
templateUrl: './positions-table.component.html',
|
||||||
styleUrls: ['./positions-table.component.scss']
|
styleUrls: ['./positions-table.component.scss']
|
||||||
})
|
})
|
||||||
export class PositionsTableComponent implements OnChanges, OnInit {
|
export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
|
||||||
@Input() baseCurrency: string;
|
@Input() baseCurrency: string;
|
||||||
@Input() deviceType: string;
|
@Input() deviceType: string;
|
||||||
@Input() locale: string;
|
@Input() locale: string;
|
||||||
@ -38,8 +40,10 @@ export class PositionsTableComponent implements OnChanges, OnInit {
|
|||||||
@ViewChild(MatPaginator) paginator: MatPaginator;
|
@ViewChild(MatPaginator) paginator: MatPaginator;
|
||||||
@ViewChild(MatSort) sort: MatSort;
|
@ViewChild(MatSort) sort: MatSort;
|
||||||
|
|
||||||
public dataSource: MatTableDataSource<PortfolioPosition> = new MatTableDataSource();
|
public dataSource: MatTableDataSource<PortfolioPosition> =
|
||||||
|
new MatTableDataSource();
|
||||||
public displayedColumns = [];
|
public displayedColumns = [];
|
||||||
|
public ignoreTypes = [Type.Cash];
|
||||||
public isLoading = true;
|
public isLoading = true;
|
||||||
public pageSize = 7;
|
public pageSize = 7;
|
||||||
public routeQueryParams: Subscription;
|
public routeQueryParams: Subscription;
|
||||||
@ -133,9 +137,12 @@ export class PositionsTableComponent implements OnChanges, OnInit {
|
|||||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||||
});
|
});
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe(() => {
|
dialogRef
|
||||||
this.router.navigate(['.'], { relativeTo: this.route });
|
.afterClosed()
|
||||||
});
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.router.navigate(['.'], { relativeTo: this.route });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
|
@import '~apps/client/src/styles/ghostfolio-style';
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
gf-position {
|
gf-position {
|
||||||
&:nth-child(even) {
|
&:nth-child(even) {
|
||||||
background-color: rgba(
|
background-color: rgba(0, 0, 0, $alpha-hover);
|
||||||
var(--dark-primary-text),
|
|
||||||
var(--palette-background-hover-alpha)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -14,10 +13,7 @@
|
|||||||
:host-context(.is-dark-theme) {
|
:host-context(.is-dark-theme) {
|
||||||
gf-position {
|
gf-position {
|
||||||
&:nth-child(even) {
|
&:nth-child(even) {
|
||||||
background-color: rgba(
|
background-color: rgba(255, 255, 255, $alpha-hover);
|
||||||
var(--light-primary-text),
|
|
||||||
var(--palette-background-hover-alpha)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,10 @@ import {
|
|||||||
OnChanges,
|
OnChanges,
|
||||||
OnInit
|
OnInit
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces';
|
import {
|
||||||
|
MarketState,
|
||||||
|
Type
|
||||||
|
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces/portfolio-position.interface';
|
import { PortfolioPosition } from '@ghostfolio/common/interfaces/portfolio-position.interface';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -25,6 +28,8 @@ export class PositionsComponent implements OnChanges, OnInit {
|
|||||||
public positionsRest: PortfolioPosition[] = [];
|
public positionsRest: PortfolioPosition[] = [];
|
||||||
public positionsWithPriority: PortfolioPosition[] = [];
|
public positionsWithPriority: PortfolioPosition[] = [];
|
||||||
|
|
||||||
|
private ignoreTypes = [Type.Cash];
|
||||||
|
|
||||||
public constructor() {}
|
public constructor() {}
|
||||||
|
|
||||||
public ngOnInit() {}
|
public ngOnInit() {}
|
||||||
@ -41,6 +46,10 @@ export class PositionsComponent implements OnChanges, OnInit {
|
|||||||
this.positionsWithPriority = [];
|
this.positionsWithPriority = [];
|
||||||
|
|
||||||
for (const [, portfolioPosition] of Object.entries(this.positions)) {
|
for (const [, portfolioPosition] of Object.entries(this.positions)) {
|
||||||
|
if (this.ignoreTypes.includes(portfolioPosition.type)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
portfolioPosition.marketState === MarketState.open ||
|
portfolioPosition.marketState === MarketState.open ||
|
||||||
this.range !== '1d'
|
this.range !== '1d'
|
||||||
|
@ -7,10 +7,7 @@
|
|||||||
padding: 0.15rem 0.75rem;
|
padding: 0.15rem 0.75rem;
|
||||||
|
|
||||||
&.mat-radio-checked {
|
&.mat-radio-checked {
|
||||||
background-color: rgba(
|
background-color: rgba(var(--dark-dividers));
|
||||||
var(--dark-primary-text),
|
|
||||||
var(--palette-foreground-divider-alpha)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
::ng-deep {
|
::ng-deep {
|
||||||
@ -33,15 +30,8 @@
|
|||||||
:host-context(.is-dark-theme) {
|
:host-context(.is-dark-theme) {
|
||||||
.mat-radio-button {
|
.mat-radio-button {
|
||||||
&.mat-radio-checked {
|
&.mat-radio-checked {
|
||||||
background-color: rgba(
|
background-color: rgba(var(--light-dividers));
|
||||||
var(--light-primary-text),
|
border: 1px solid rgba(var(--light-disabled-text));
|
||||||
var(--palette-foreground-divider-alpha)
|
|
||||||
);
|
|
||||||
border: 1px solid
|
|
||||||
rgba(
|
|
||||||
var(--light-primary-text),
|
|
||||||
var(--palette-foreground-disabled-button-alpha)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
::ng-deep {
|
::ng-deep {
|
||||||
|
@ -202,17 +202,45 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="actions">
|
<ng-container matColumnDef="actions">
|
||||||
<th *matHeaderCellDef class="px-1 text-center" i18n mat-header-cell></th>
|
<th *matHeaderCellDef class="px-1 text-center" mat-header-cell>
|
||||||
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
|
||||||
<button
|
<button
|
||||||
class="mx-1 no-min-width px-2"
|
class="mx-1 no-min-width px-2"
|
||||||
mat-button
|
mat-button
|
||||||
[matMenuTriggerFor]="accountMenu"
|
[matMenuTriggerFor]="transactionsMenu"
|
||||||
(click)="$event.stopPropagation()"
|
(click)="$event.stopPropagation()"
|
||||||
>
|
>
|
||||||
<ion-icon name="ellipsis-vertical"></ion-icon>
|
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||||
</button>
|
</button>
|
||||||
<mat-menu #accountMenu="matMenu" xPosition="before">
|
<mat-menu #transactionsMenu="matMenu" xPosition="before">
|
||||||
|
<button
|
||||||
|
*ngIf="hasPermissionToImportOrders"
|
||||||
|
class="align-items-center d-flex"
|
||||||
|
mat-menu-item
|
||||||
|
(click)="onImport()"
|
||||||
|
>
|
||||||
|
<ion-icon class="mr-2" name="cloud-upload-outline"></ion-icon>
|
||||||
|
<span i18n>Import</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="align-items-center d-flex"
|
||||||
|
mat-menu-item
|
||||||
|
(click)="onExport()"
|
||||||
|
>
|
||||||
|
<ion-icon class="mr-2" name="cloud-download-outline"></ion-icon>
|
||||||
|
<span i18n>Export</span>
|
||||||
|
</button>
|
||||||
|
</mat-menu>
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
||||||
|
<button
|
||||||
|
class="mx-1 no-min-width px-2"
|
||||||
|
mat-button
|
||||||
|
[matMenuTriggerFor]="transactionMenu"
|
||||||
|
(click)="$event.stopPropagation()"
|
||||||
|
>
|
||||||
|
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||||
|
</button>
|
||||||
|
<mat-menu #transactionMenu="matMenu" xPosition="before">
|
||||||
<button i18n mat-menu-item (click)="onUpdateTransaction(element)">
|
<button i18n mat-menu-item (click)="onUpdateTransaction(element)">
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
.type-badge {
|
.type-badge {
|
||||||
background-color: rgba(var(--dark-primary-text), 0.05);
|
background-color: rgba(var(--palette-foreground-text), 0.05);
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
|
|
||||||
@ -54,7 +54,10 @@
|
|||||||
|
|
||||||
.mat-table {
|
.mat-table {
|
||||||
.type-badge {
|
.type-badge {
|
||||||
background-color: rgba(var(--light-primary-text), 0.1);
|
background-color: rgba(
|
||||||
|
var(--palette-foreground-text-dark),
|
||||||
|
0.1
|
||||||
|
) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,10 +43,13 @@ export class TransactionsTableComponent
|
|||||||
{
|
{
|
||||||
@Input() baseCurrency: string;
|
@Input() baseCurrency: string;
|
||||||
@Input() deviceType: string;
|
@Input() deviceType: string;
|
||||||
|
@Input() hasPermissionToImportOrders: boolean;
|
||||||
@Input() locale: string;
|
@Input() locale: string;
|
||||||
@Input() showActions: boolean;
|
@Input() showActions: boolean;
|
||||||
@Input() transactions: OrderWithAccount[];
|
@Input() transactions: OrderWithAccount[];
|
||||||
|
|
||||||
|
@Output() export = new EventEmitter<void>();
|
||||||
|
@Output() import = new EventEmitter<void>();
|
||||||
@Output() transactionDeleted = new EventEmitter<string>();
|
@Output() transactionDeleted = new EventEmitter<string>();
|
||||||
@Output() transactionToClone = new EventEmitter<OrderWithAccount>();
|
@Output() transactionToClone = new EventEmitter<OrderWithAccount>();
|
||||||
@Output() transactionToUpdate = new EventEmitter<OrderWithAccount>();
|
@Output() transactionToUpdate = new EventEmitter<OrderWithAccount>();
|
||||||
@ -89,18 +92,20 @@ export class TransactionsTableComponent
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.searchControl.valueChanges.subscribe((keyword) => {
|
this.searchControl.valueChanges
|
||||||
if (keyword) {
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
const filterValue = keyword.toLowerCase();
|
.subscribe((keyword) => {
|
||||||
this.filters$.next(
|
if (keyword) {
|
||||||
this.allFilters.filter(
|
const filterValue = keyword.toLowerCase();
|
||||||
(filter) => filter.toLowerCase().indexOf(filterValue) === 0
|
this.filters$.next(
|
||||||
)
|
this.allFilters.filter(
|
||||||
);
|
(filter) => filter.toLowerCase().indexOf(filterValue) === 0
|
||||||
} else {
|
)
|
||||||
this.filters$.next(this.allFilters);
|
);
|
||||||
}
|
} else {
|
||||||
});
|
this.filters$.next(this.allFilters);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public addKeyword({ input, value }: MatChipInputEvent): void {
|
public addKeyword({ input, value }: MatChipInputEvent): void {
|
||||||
@ -183,6 +188,14 @@ export class TransactionsTableComponent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onExport() {
|
||||||
|
this.export.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
public onImport() {
|
||||||
|
this.import.emit();
|
||||||
|
}
|
||||||
|
|
||||||
public onOpenPositionDialog({
|
public onOpenPositionDialog({
|
||||||
symbol,
|
symbol,
|
||||||
title
|
title
|
||||||
@ -223,9 +236,12 @@ export class TransactionsTableComponent
|
|||||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||||
});
|
});
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe(() => {
|
dialogRef
|
||||||
this.router.navigate(['.'], { relativeTo: this.route });
|
.afterClosed()
|
||||||
});
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.router.navigate(['.'], { relativeTo: this.route });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
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 { baseCurrency } from '@ghostfolio/common/config';
|
import { baseCurrency } from '@ghostfolio/common/config';
|
||||||
@ -15,7 +15,7 @@ import { environment } from '../../../environments/environment';
|
|||||||
templateUrl: './about-page.html',
|
templateUrl: './about-page.html',
|
||||||
styleUrls: ['./about-page.scss']
|
styleUrls: ['./about-page.scss']
|
||||||
})
|
})
|
||||||
export class AboutPageComponent implements OnInit {
|
export class AboutPageComponent implements OnDestroy, OnInit {
|
||||||
public baseCurrency = baseCurrency;
|
public baseCurrency = baseCurrency;
|
||||||
public hasPermissionForStatistics: boolean;
|
public hasPermissionForStatistics: boolean;
|
||||||
public isLoggedIn: boolean;
|
public isLoggedIn: boolean;
|
||||||
@ -39,18 +39,13 @@ export class AboutPageComponent implements OnInit {
|
|||||||
* Initializes the controller
|
* Initializes the controller
|
||||||
*/
|
*/
|
||||||
public ngOnInit() {
|
public ngOnInit() {
|
||||||
this.dataService
|
const { globalPermissions, statistics } = this.dataService.fetchInfo();
|
||||||
.fetchInfo()
|
this.hasPermissionForStatistics = hasPermission(
|
||||||
.subscribe(({ globalPermissions, statistics }) => {
|
globalPermissions,
|
||||||
this.hasPermissionForStatistics = hasPermission(
|
permissions.enableStatistics
|
||||||
globalPermissions,
|
);
|
||||||
permissions.enableStatistics
|
|
||||||
);
|
|
||||||
|
|
||||||
this.statistics = statistics;
|
this.statistics = statistics;
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.userService.stateChanged
|
this.userService.stateChanged
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
@ -5,11 +5,18 @@
|
|||||||
<mat-card>
|
<mat-card>
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<p>
|
<p>
|
||||||
<strong>Ghostfolio</strong> is open source software which empowers
|
<strong>Ghostfolio</strong> is a lightweight wealth management
|
||||||
busy folks to have a sharp look of their financial assets and to
|
application for individuals to keep track of their wealth like
|
||||||
make solid, data-driven investment decisions by evaluating automated
|
stocks, ETFs or cryptocurrencies and make solid, data-driven
|
||||||
static portfolio analysis rules. The project has been initiated by
|
investment decisions. The source code is fully available as open
|
||||||
<a href="https://dotsilver.ch">Thomas Kaul</a>.
|
source software (OSS). The project has been initiated by
|
||||||
|
<a href="https://dotsilver.ch">Thomas Kaul</a> and is driven by the
|
||||||
|
efforts of its
|
||||||
|
<a
|
||||||
|
href="https://github.com/ghostfolio/ghostfolio/graphs/contributors"
|
||||||
|
title="Contributors to Ghostfolio"
|
||||||
|
>contributors</a
|
||||||
|
>.
|
||||||
<ng-container *ngIf="lastPublish">
|
<ng-container *ngIf="lastPublish">
|
||||||
This instance is running Ghostfolio {{ version }} and has been
|
This instance is running Ghostfolio {{ version }} and has been
|
||||||
last published on {{ lastPublish }}.</ng-container
|
last published on {{ lastPublish }}.</ng-container
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
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 { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
|
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
|
||||||
import { baseCurrency, DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
|
import { DEFAULT_DATE_FORMAT, baseCurrency } from '@ghostfolio/common/config';
|
||||||
import { Access, User } from '@ghostfolio/common/interfaces';
|
import { Access, User } from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { Currency } from '@prisma/client';
|
import { Currency } from '@prisma/client';
|
||||||
@ -54,24 +54,19 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
|||||||
private userService: UserService,
|
private userService: UserService,
|
||||||
public webAuthnService: WebAuthnService
|
public webAuthnService: WebAuthnService
|
||||||
) {
|
) {
|
||||||
this.dataService
|
const { currencies, globalPermissions, subscriptions } =
|
||||||
.fetchInfo()
|
this.dataService.fetchInfo();
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
this.coupon = subscriptions?.[0]?.coupon;
|
||||||
.subscribe(({ currencies, globalPermissions, subscriptions }) => {
|
this.couponId = subscriptions?.[0]?.couponId;
|
||||||
this.coupon = subscriptions?.[0]?.coupon;
|
this.currencies = currencies;
|
||||||
this.couponId = subscriptions?.[0]?.couponId;
|
|
||||||
this.currencies = currencies;
|
|
||||||
|
|
||||||
this.hasPermissionForSubscription = hasPermission(
|
this.hasPermissionForSubscription = hasPermission(
|
||||||
globalPermissions,
|
globalPermissions,
|
||||||
permissions.enableSubscription
|
permissions.enableSubscription
|
||||||
);
|
);
|
||||||
|
|
||||||
this.price = subscriptions?.[0]?.price;
|
this.price = subscriptions?.[0]?.price;
|
||||||
this.priceId = subscriptions?.[0]?.priceId;
|
this.priceId = subscriptions?.[0]?.priceId;
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.userService.stateChanged
|
this.userService.stateChanged
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
@ -166,6 +161,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
|||||||
this.webAuthnService
|
this.webAuthnService
|
||||||
.deregister()
|
.deregister()
|
||||||
.pipe(
|
.pipe(
|
||||||
|
takeUntil(this.unsubscribeSubject),
|
||||||
catchError(() => {
|
catchError(() => {
|
||||||
this.update();
|
this.update();
|
||||||
|
|
||||||
@ -181,6 +177,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
|||||||
this.webAuthnService
|
this.webAuthnService
|
||||||
.register()
|
.register()
|
||||||
.pipe(
|
.pipe(
|
||||||
|
takeUntil(this.unsubscribeSubject),
|
||||||
catchError(() => {
|
catchError(() => {
|
||||||
this.update();
|
this.update();
|
||||||
|
|
||||||
|
@ -16,14 +16,19 @@
|
|||||||
<div class="w-50" i18n>Membership</div>
|
<div class="w-50" i18n>Membership</div>
|
||||||
<div class="w-50">
|
<div class="w-50">
|
||||||
<div class="align-items-center d-flex mb-1">
|
<div class="align-items-center d-flex mb-1">
|
||||||
{{ user.subscription.type }}
|
{{ user?.subscription?.type }}
|
||||||
|
<ion-icon
|
||||||
|
*ngIf="user?.subscription?.type === 'Premium'"
|
||||||
|
class="ml-1 text-muted"
|
||||||
|
name="diamond-outline"
|
||||||
|
></ion-icon>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="user.subscription.expiresAt">
|
<div *ngIf="user?.subscription?.expiresAt">
|
||||||
Valid until {{ user.subscription.expiresAt | date:
|
Valid until {{ user?.subscription?.expiresAt | date:
|
||||||
defaultDateFormat }}
|
defaultDateFormat }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
*ngIf="hasPermissionForSubscription && !user.subscription.expiresAt"
|
*ngIf="hasPermissionForSubscription && !user?.subscription?.expiresAt"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
color="primary"
|
color="primary"
|
||||||
@ -46,45 +51,54 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex mt-4 py-1">
|
<div class="d-flex mt-4 py-1">
|
||||||
<div class="pt-4 w-50" i18n>Settings</div>
|
<form #changeUserSettingsForm="ngForm" class="w-100">
|
||||||
<div class="w-50">
|
<div class="d-flex mb-2">
|
||||||
<form #changeUserSettingsForm="ngForm">
|
<div class="align-items-center d-flex pt-1 w-50" i18n>
|
||||||
<mat-form-field appearance="outline" class="mb-3 w-100">
|
Base Currency
|
||||||
<mat-label i18n>Base Currency</mat-label>
|
</div>
|
||||||
<mat-select
|
<div class="w-50">
|
||||||
name="baseCurrency"
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
[disabled]="!hasPermissionToUpdateUserSettings"
|
|
||||||
[value]="user.settings.baseCurrency"
|
|
||||||
(selectionChange)="onChangeUserSettings('baseCurrency', $event.value)"
|
|
||||||
>
|
|
||||||
<mat-option
|
|
||||||
*ngFor="let currency of currencies"
|
|
||||||
[value]="currency"
|
|
||||||
>{{ currency }}</mat-option
|
|
||||||
>
|
|
||||||
</mat-select>
|
|
||||||
</mat-form-field>
|
|
||||||
<div class="align-items-center d-flex overflow-hidden">
|
|
||||||
<mat-form-field appearance="outline" class="flex-grow-1">
|
|
||||||
<mat-label i18n>View Mode</mat-label>
|
|
||||||
<mat-select
|
<mat-select
|
||||||
name="viewMode"
|
name="baseCurrency"
|
||||||
[disabled]="!hasPermissionToUpdateViewMode"
|
[disabled]="!hasPermissionToUpdateUserSettings"
|
||||||
[value]="user.settings.viewMode"
|
[value]="user.settings.baseCurrency"
|
||||||
(selectionChange)="onChangeUserSettings('viewMode', $event.value)"
|
(selectionChange)="onChangeUserSettings('baseCurrency', $event.value)"
|
||||||
>
|
>
|
||||||
<mat-option value="DEFAULT">Default</mat-option>
|
<mat-option
|
||||||
<mat-option value="ZEN">Zen</mat-option>
|
*ngFor="let currency of currencies"
|
||||||
|
[value]="currency"
|
||||||
|
>{{ currency }}</mat-option
|
||||||
|
>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="align-items-center d-flex pt-1 w-50" i18n>
|
||||||
|
View Mode
|
||||||
<ion-icon
|
<ion-icon
|
||||||
*ngIf="!hasPermissionToUpdateViewMode"
|
*ngIf="!hasPermissionToUpdateViewMode"
|
||||||
class="h5 mb-0 mx-3 text-muted"
|
class="mx-1 text-muted"
|
||||||
name="diamond-outline"
|
name="diamond-outline"
|
||||||
></ion-icon>
|
></ion-icon>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
<div class="w-50">
|
||||||
</div>
|
<div class="align-items-center d-flex overflow-hidden">
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-select
|
||||||
|
name="viewMode"
|
||||||
|
[disabled]="!hasPermissionToUpdateViewMode"
|
||||||
|
[value]="user.settings.viewMode"
|
||||||
|
(selectionChange)="onChangeUserSettings('viewMode', $event.value)"
|
||||||
|
>
|
||||||
|
<mat-option value="DEFAULT">Default</mat-option>
|
||||||
|
<mat-option value="ZEN">Zen</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="align-items-center d-flex mt-4 py-1">
|
<div class="align-items-center d-flex mt-4 py-1">
|
||||||
<div class="w-50" i18n>Sign in with fingerprint</div>
|
<div class="w-50" i18n>Sign in with fingerprint</div>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/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';
|
||||||
@ -20,7 +20,7 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog/c
|
|||||||
templateUrl: './accounts-page.html',
|
templateUrl: './accounts-page.html',
|
||||||
styleUrls: ['./accounts-page.scss']
|
styleUrls: ['./accounts-page.scss']
|
||||||
})
|
})
|
||||||
export class AccountsPageComponent implements OnInit {
|
export class AccountsPageComponent implements OnDestroy, OnInit {
|
||||||
public accounts: AccountModel[];
|
public accounts: AccountModel[];
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
public hasImpersonationId: boolean;
|
public hasImpersonationId: boolean;
|
||||||
@ -71,6 +71,7 @@ export class AccountsPageComponent implements OnInit {
|
|||||||
|
|
||||||
this.impersonationStorageService
|
this.impersonationStorageService
|
||||||
.onChangeHasImpersonation()
|
.onChangeHasImpersonation()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((aId) => {
|
.subscribe((aId) => {
|
||||||
this.hasImpersonationId = !!aId;
|
this.hasImpersonationId = !!aId;
|
||||||
});
|
});
|
||||||
@ -98,23 +99,29 @@ export class AccountsPageComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public fetchAccounts() {
|
public fetchAccounts() {
|
||||||
this.dataService.fetchAccounts().subscribe((response) => {
|
this.dataService
|
||||||
this.accounts = response;
|
.fetchAccounts()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((response) => {
|
||||||
|
this.accounts = response;
|
||||||
|
|
||||||
if (this.accounts?.length <= 0) {
|
if (this.accounts?.length <= 0) {
|
||||||
this.router.navigate([], { queryParams: { createDialog: true } });
|
this.router.navigate([], { queryParams: { createDialog: true } });
|
||||||
}
|
}
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onDeleteAccount(aId: string) {
|
public onDeleteAccount(aId: string) {
|
||||||
this.dataService.deleteAccount(aId).subscribe({
|
this.dataService
|
||||||
next: () => {
|
.deleteAccount(aId)
|
||||||
this.fetchAccounts();
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
}
|
.subscribe({
|
||||||
});
|
next: () => {
|
||||||
|
this.fetchAccounts();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onUpdateAccount(aAccount: AccountModel) {
|
public onUpdateAccount(aAccount: AccountModel) {
|
||||||
@ -125,6 +132,8 @@ export class AccountsPageComponent implements OnInit {
|
|||||||
|
|
||||||
public openUpdateAccountDialog({
|
public openUpdateAccountDialog({
|
||||||
accountType,
|
accountType,
|
||||||
|
balance,
|
||||||
|
currency,
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
platformId
|
platformId
|
||||||
@ -133,6 +142,8 @@ export class AccountsPageComponent implements OnInit {
|
|||||||
data: {
|
data: {
|
||||||
account: {
|
account: {
|
||||||
accountType,
|
accountType,
|
||||||
|
balance,
|
||||||
|
currency,
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
platformId
|
platformId
|
||||||
@ -142,19 +153,25 @@ export class AccountsPageComponent implements OnInit {
|
|||||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||||
});
|
});
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe((data: any) => {
|
dialogRef
|
||||||
const account: UpdateAccountDto = data?.account;
|
.afterClosed()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((data: any) => {
|
||||||
|
const account: UpdateAccountDto = data?.account;
|
||||||
|
|
||||||
if (account) {
|
if (account) {
|
||||||
this.dataService.putAccount(account).subscribe({
|
this.dataService
|
||||||
next: () => {
|
.putAccount(account)
|
||||||
this.fetchAccounts();
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
}
|
.subscribe({
|
||||||
});
|
next: () => {
|
||||||
}
|
this.fetchAccounts();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.router.navigate(['.'], { relativeTo: this.route });
|
this.router.navigate(['.'], { relativeTo: this.route });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
@ -167,6 +184,8 @@ export class AccountsPageComponent implements OnInit {
|
|||||||
data: {
|
data: {
|
||||||
account: {
|
account: {
|
||||||
accountType: AccountType.SECURITIES,
|
accountType: AccountType.SECURITIES,
|
||||||
|
balance: 0,
|
||||||
|
currency: this.user?.settings?.baseCurrency,
|
||||||
name: null,
|
name: null,
|
||||||
platformId: null
|
platformId: null
|
||||||
}
|
}
|
||||||
@ -175,18 +194,24 @@ export class AccountsPageComponent implements OnInit {
|
|||||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||||
});
|
});
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe((data: any) => {
|
dialogRef
|
||||||
const account: CreateAccountDto = data?.account;
|
.afterClosed()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((data: any) => {
|
||||||
|
const account: CreateAccountDto = data?.account;
|
||||||
|
|
||||||
if (account) {
|
if (account) {
|
||||||
this.dataService.postAccount(account).subscribe({
|
this.dataService
|
||||||
next: () => {
|
.postAccount(account)
|
||||||
this.fetchAccounts();
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
}
|
.subscribe({
|
||||||
});
|
next: () => {
|
||||||
}
|
this.fetchAccounts();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.router.navigate(['.'], { relativeTo: this.route });
|
this.router.navigate(['.'], { relativeTo: this.route });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,10 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
Inject,
|
||||||
|
OnDestroy
|
||||||
|
} from '@angular/core';
|
||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { Currency } from '@prisma/client';
|
import { Currency } from '@prisma/client';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
@ -13,23 +19,24 @@ import { CreateOrUpdateAccountDialogParams } from './interfaces/interfaces';
|
|||||||
styleUrls: ['./create-or-update-account-dialog.scss'],
|
styleUrls: ['./create-or-update-account-dialog.scss'],
|
||||||
templateUrl: 'create-or-update-account-dialog.html'
|
templateUrl: 'create-or-update-account-dialog.html'
|
||||||
})
|
})
|
||||||
export class CreateOrUpdateAccountDialog {
|
export class CreateOrUpdateAccountDialog implements OnDestroy {
|
||||||
public currencies: Currency[] = [];
|
public currencies: Currency[] = [];
|
||||||
public platforms: { id: string; name: string }[];
|
public platforms: { id: string; name: string }[];
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
public dialogRef: MatDialogRef<CreateOrUpdateAccountDialog>,
|
public dialogRef: MatDialogRef<CreateOrUpdateAccountDialog>,
|
||||||
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateAccountDialogParams
|
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateAccountDialogParams
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.dataService.fetchInfo().subscribe(({ currencies, platforms }) => {
|
const { currencies, platforms } = this.dataService.fetchInfo();
|
||||||
this.currencies = currencies;
|
|
||||||
this.platforms = platforms;
|
this.currencies = currencies;
|
||||||
});
|
this.platforms = platforms;
|
||||||
}
|
}
|
||||||
|
|
||||||
public onCancel(): void {
|
public onCancel(): void {
|
||||||
|
@ -8,14 +8,37 @@
|
|||||||
<input matInput name="name" required [(ngModel)]="data.account.name" />
|
<input matInput name="name" required [(ngModel)]="data.account.name" />
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-none">
|
<div>
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
<mat-label i18n>Type</mat-label>
|
<mat-label i18n>Type</mat-label>
|
||||||
<mat-select name="type" required [(value)]="data.account.accountType">
|
<mat-select name="type" required [(value)]="data.account.accountType">
|
||||||
<mat-option value="SECURITIES" i18n> SECURITIES </mat-option>
|
<mat-option value="CASH" i18n>Cash</mat-option>
|
||||||
|
<mat-option value="SECURITIES" i18n>Securities</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label i18n>Currency</mat-label>
|
||||||
|
<mat-select name="currency" required [(value)]="data.account.currency">
|
||||||
|
<mat-option *ngFor="let currency of currencies" [value]="currency"
|
||||||
|
>{{ currency }}</mat-option
|
||||||
|
>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label i18n>Balance</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
name="balance"
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
[(ngModel)]="data.account.balance"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
<mat-label i18n>Platform</mat-label>
|
<mat-label i18n>Platform</mat-label>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||||
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';
|
||||||
@ -19,7 +19,7 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
templateUrl: './admin-page.html',
|
templateUrl: './admin-page.html',
|
||||||
styleUrls: ['./admin-page.scss']
|
styleUrls: ['./admin-page.scss']
|
||||||
})
|
})
|
||||||
export class AdminPageComponent implements OnInit {
|
export class AdminPageComponent implements OnDestroy, OnInit {
|
||||||
public dataGatheringInProgress: boolean;
|
public dataGatheringInProgress: boolean;
|
||||||
public defaultDateFormat = DEFAULT_DATE_FORMAT;
|
public defaultDateFormat = DEFAULT_DATE_FORMAT;
|
||||||
public exchangeRates: { label1: string; label2: string; value: number }[];
|
public exchangeRates: { label1: string; label2: string; value: number }[];
|
||||||
@ -58,11 +58,14 @@ export class AdminPageComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onFlushCache() {
|
public onFlushCache() {
|
||||||
this.cacheService.flush().subscribe(() => {
|
this.cacheService
|
||||||
setTimeout(() => {
|
.flush()
|
||||||
window.location.reload();
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
}, 300);
|
.subscribe(() => {
|
||||||
});
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onGatherMax() {
|
public onGatherMax() {
|
||||||
@ -71,11 +74,14 @@ export class AdminPageComponent implements OnInit {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (confirmation === true) {
|
if (confirmation === true) {
|
||||||
this.adminService.gatherMax().subscribe(() => {
|
this.adminService
|
||||||
setTimeout(() => {
|
.gatherMax()
|
||||||
window.location.reload();
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
}, 300);
|
.subscribe(() => {
|
||||||
});
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,11 +104,14 @@ export class AdminPageComponent implements OnInit {
|
|||||||
const confirmation = confirm('Do you really want to delete this user?');
|
const confirmation = confirm('Do you really want to delete this user?');
|
||||||
|
|
||||||
if (confirmation) {
|
if (confirmation) {
|
||||||
this.dataService.deleteUser(aId).subscribe({
|
this.dataService
|
||||||
next: () => {
|
.deleteUser(aId)
|
||||||
this.fetchAdminData();
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
}
|
.subscribe({
|
||||||
});
|
next: () => {
|
||||||
|
this.fetchAdminData();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,17 +1,21 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import {
|
import {
|
||||||
STAY_SIGNED_IN,
|
STAY_SIGNED_IN,
|
||||||
SettingsStorageService
|
SettingsStorageService
|
||||||
} from '@ghostfolio/client/services/settings-storage.service';
|
} from '@ghostfolio/client/services/settings-storage.service';
|
||||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'gf-auth-page',
|
selector: 'gf-auth-page',
|
||||||
templateUrl: './auth-page.html',
|
templateUrl: './auth-page.html',
|
||||||
styleUrls: ['./auth-page.scss']
|
styleUrls: ['./auth-page.scss']
|
||||||
})
|
})
|
||||||
export class AuthPageComponent implements OnInit {
|
export class AuthPageComponent implements OnDestroy, OnInit {
|
||||||
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
@ -26,14 +30,21 @@ export class AuthPageComponent implements OnInit {
|
|||||||
* Initializes the controller
|
* Initializes the controller
|
||||||
*/
|
*/
|
||||||
public ngOnInit() {
|
public ngOnInit() {
|
||||||
this.route.params.subscribe((params) => {
|
this.route.params
|
||||||
const jwt = params['jwt'];
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
this.tokenStorageService.saveToken(
|
.subscribe((params) => {
|
||||||
jwt,
|
const jwt = params['jwt'];
|
||||||
this.settingsStorageService.getSetting(STAY_SIGNED_IN) === 'true'
|
this.tokenStorageService.saveToken(
|
||||||
);
|
jwt,
|
||||||
|
this.settingsStorageService.getSetting(STAY_SIGNED_IN) === 'true'
|
||||||
|
);
|
||||||
|
|
||||||
this.router.navigate(['/']);
|
this.router.navigate(['/']);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -116,6 +116,7 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
this.impersonationStorageService
|
this.impersonationStorageService
|
||||||
.onChangeHasImpersonation()
|
.onChangeHasImpersonation()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((aId) => {
|
.subscribe((aId) => {
|
||||||
this.hasImpersonationId = !!aId;
|
this.hasImpersonationId = !!aId;
|
||||||
});
|
});
|
||||||
@ -148,9 +149,12 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
|||||||
width: '50rem'
|
width: '50rem'
|
||||||
});
|
});
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe(() => {
|
dialogRef
|
||||||
this.router.navigate(['.'], { relativeTo: this.route });
|
.afterClosed()
|
||||||
});
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.router.navigate(['.'], { relativeTo: this.route });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private update() {
|
private update() {
|
||||||
@ -161,6 +165,7 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchChart({ range: this.dateRange })
|
.fetchChart({ range: this.dateRange })
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((chartData) => {
|
.subscribe((chartData) => {
|
||||||
this.historicalDataItems = chartData.map((chartDataItem) => {
|
this.historicalDataItems = chartData.map((chartDataItem) => {
|
||||||
return {
|
return {
|
||||||
@ -174,6 +179,7 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchPortfolioPerformance({ range: this.dateRange })
|
.fetchPortfolioPerformance({ range: this.dateRange })
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((response) => {
|
.subscribe((response) => {
|
||||||
this.performance = response;
|
this.performance = response;
|
||||||
this.isLoadingPerformance = false;
|
this.isLoadingPerformance = false;
|
||||||
@ -181,15 +187,19 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
|||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.dataService.fetchPortfolioOverview().subscribe((response) => {
|
this.dataService
|
||||||
this.overview = response;
|
.fetchPortfolioOverview()
|
||||||
this.isLoadingOverview = false;
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((response) => {
|
||||||
|
this.overview = response;
|
||||||
|
this.isLoadingOverview = false;
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchPortfolioPositions({ range: this.dateRange })
|
.fetchPortfolioPositions({ range: this.dateRange })
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((response) => {
|
.subscribe((response) => {
|
||||||
this.positions = response;
|
this.positions = response;
|
||||||
this.hasPositions =
|
this.hasPositions =
|
||||||
|
@ -3,7 +3,6 @@ import { Router } from '@angular/router';
|
|||||||
import { LineChartItem } from '@ghostfolio/client/components/line-chart/interfaces/line-chart.interface';
|
import { LineChartItem } from '@ghostfolio/client/components/line-chart/interfaces/line-chart.interface';
|
||||||
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';
|
||||||
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
|
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
@ -33,13 +32,11 @@ export class LandingPageComponent implements OnDestroy, OnInit {
|
|||||||
* Initializes the controller
|
* Initializes the controller
|
||||||
*/
|
*/
|
||||||
public ngOnInit() {
|
public ngOnInit() {
|
||||||
this.dataService.fetchInfo().subscribe(({ demoAuthToken }) => {
|
const { demoAuthToken } = this.dataService.fetchInfo();
|
||||||
this.demoAuthToken = demoAuthToken;
|
|
||||||
|
|
||||||
this.initializeLineChart();
|
this.demoAuthToken = demoAuthToken;
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.initializeLineChart();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public initializeLineChart() {
|
public initializeLineChart() {
|
||||||
|
@ -1,16 +1,29 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div
|
<div
|
||||||
class="align-items-center d-flex flex-column justify-content-center mb-4 w-100"
|
class="
|
||||||
|
align-items-center
|
||||||
|
d-flex
|
||||||
|
flex-column
|
||||||
|
justify-content-center
|
||||||
|
mb-4
|
||||||
|
w-100
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<gf-logo size="large"></gf-logo>
|
<gf-logo size="large"></gf-logo>
|
||||||
<p class="lead m-0">Open Source Portfolio Tracker</p>
|
<p class="lead m-0">Wealth Management Software</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="button-container row">
|
<div class="button-container row">
|
||||||
<div
|
<div
|
||||||
class="align-items-center col d-flex justify-content-center position-relative"
|
class="
|
||||||
|
align-items-center
|
||||||
|
col
|
||||||
|
d-flex
|
||||||
|
justify-content-center
|
||||||
|
position-relative
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<div class="py-5 text-center">
|
<div class="py-5 text-center">
|
||||||
<a
|
<a
|
||||||
@ -51,10 +64,9 @@
|
|||||||
<strong>personal investment strategy</strong>.
|
<strong>personal investment strategy</strong>.
|
||||||
</h2>
|
</h2>
|
||||||
<p class="lead">
|
<p class="lead">
|
||||||
<strong>Ghostfolio</strong> empowers busy folks to have a sharp look of
|
<strong>Ghostfolio</strong> empowers busy people to keep track of their
|
||||||
their financial assets and to make solid, data-driven investment
|
wealth like stocks, ETFs or cryptocurrencies and make solid, data-driven
|
||||||
decisions by evaluating automated
|
investment decisions.
|
||||||
<strong>Static Portfolio Analysis Rules</strong>.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
.button-container {
|
.button-container {
|
||||||
aspect-ratio: 16 / 9;
|
aspect-ratio: 16 / 9;
|
||||||
margin-top: -4rem;
|
|
||||||
|
|
||||||
gf-line-chart {
|
gf-line-chart {
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
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 { baseCurrency } from '@ghostfolio/common/config';
|
import { baseCurrency } from '@ghostfolio/common/config';
|
||||||
@ -11,7 +11,7 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
templateUrl: './pricing-page.html',
|
templateUrl: './pricing-page.html',
|
||||||
styleUrls: ['./pricing-page.scss']
|
styleUrls: ['./pricing-page.scss']
|
||||||
})
|
})
|
||||||
export class PricingPageComponent implements OnInit {
|
export class PricingPageComponent implements OnDestroy, OnInit {
|
||||||
public baseCurrency = baseCurrency;
|
public baseCurrency = baseCurrency;
|
||||||
public coupon: number;
|
public coupon: number;
|
||||||
public isLoggedIn: boolean;
|
public isLoggedIn: boolean;
|
||||||
@ -28,15 +28,10 @@ export class PricingPageComponent implements OnInit {
|
|||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
private userService: UserService
|
private userService: UserService
|
||||||
) {
|
) {
|
||||||
this.dataService
|
const { subscriptions } = this.dataService.fetchInfo();
|
||||||
.fetchInfo()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe(({ subscriptions }) => {
|
|
||||||
this.coupon = this.price = subscriptions?.[0]?.coupon;
|
|
||||||
this.price = subscriptions?.[0]?.price;
|
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.coupon = this.price = subscriptions?.[0]?.coupon;
|
||||||
});
|
this.price = subscriptions?.[0]?.price;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -8,9 +8,10 @@
|
|||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<p>
|
<p>
|
||||||
Our official
|
Our official
|
||||||
<strong>Ghostfolio</strong> cloud offering is the easiest way to get
|
<strong>Ghostfolio Premium</strong> cloud offering is the easiest
|
||||||
started. Due to the time it saves, this will be the best option for
|
way to get started. Due to the time it saves, this will be the best
|
||||||
most people. The revenue is used for covering the hosting costs.
|
option for most people. The revenue is used for covering the hosting
|
||||||
|
costs.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
If you prefer to run <strong>Ghostfolio</strong> on your own
|
If you prefer to run <strong>Ghostfolio</strong> on your own
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
.mat-card {
|
.mat-card {
|
||||||
&.active {
|
&.active {
|
||||||
border-color: rgba(var(--palette-primary-500), 1);
|
border-color: rgba(var(--palette-primary-500), 1);
|
||||||
|
box-shadow: 0 0 0 1px rgba(var(--palette-primary-500), 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,17 +41,13 @@ export class RegisterPageComponent implements OnDestroy, OnInit {
|
|||||||
* Initializes the controller
|
* Initializes the controller
|
||||||
*/
|
*/
|
||||||
public ngOnInit() {
|
public ngOnInit() {
|
||||||
this.dataService
|
const { demoAuthToken, globalPermissions } = this.dataService.fetchInfo();
|
||||||
.fetchInfo()
|
|
||||||
.subscribe(({ demoAuthToken, globalPermissions }) => {
|
|
||||||
this.demoAuthToken = demoAuthToken;
|
|
||||||
this.hasPermissionForSocialLogin = hasPermission(
|
|
||||||
globalPermissions,
|
|
||||||
permissions.enableSocialLogin
|
|
||||||
);
|
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.demoAuthToken = demoAuthToken;
|
||||||
});
|
this.hasPermissionForSocialLogin = hasPermission(
|
||||||
|
globalPermissions,
|
||||||
|
permissions.enableSocialLogin
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createAccount() {
|
public async createAccount() {
|
||||||
@ -76,13 +72,16 @@ export class RegisterPageComponent implements OnDestroy, OnInit {
|
|||||||
width: '30rem'
|
width: '30rem'
|
||||||
});
|
});
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe((data) => {
|
dialogRef
|
||||||
if (data?.authToken) {
|
.afterClosed()
|
||||||
this.tokenStorageService.saveToken(authToken, true);
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((data) => {
|
||||||
|
if (data?.authToken) {
|
||||||
|
this.tokenStorageService.saveToken(authToken, true);
|
||||||
|
|
||||||
this.router.navigate(['/']);
|
this.router.navigate(['/']);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
|
@ -1,9 +1,4 @@
|
|||||||
import {
|
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
|
||||||
ChangeDetectionStrategy,
|
|
||||||
ChangeDetectorRef,
|
|
||||||
Component,
|
|
||||||
Inject
|
|
||||||
} from '@angular/core';
|
|
||||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -15,18 +10,11 @@ import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
|||||||
export class ShowAccessTokenDialog {
|
export class ShowAccessTokenDialog {
|
||||||
public isAgreeButtonDisabled = true;
|
public isAgreeButtonDisabled = true;
|
||||||
|
|
||||||
public constructor(
|
public constructor(@Inject(MAT_DIALOG_DATA) public data: any) {}
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
|
||||||
@Inject(MAT_DIALOG_DATA) public data: any
|
|
||||||
) {}
|
|
||||||
|
|
||||||
ngOnInit() {}
|
ngOnInit() {}
|
||||||
|
|
||||||
public enableAgreeButton() {
|
public enableAgreeButton() {
|
||||||
setTimeout(() => {
|
this.isAgreeButtonDisabled = false;
|
||||||
this.isAgreeButtonDisabled = false;
|
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
|
||||||
}, 1500);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,11 +32,11 @@
|
|||||||
<button i18n mat-flat-button [mat-dialog-close]="undefined">Cancel</button>
|
<button i18n mat-flat-button [mat-dialog-close]="undefined">Cancel</button>
|
||||||
<button
|
<button
|
||||||
color="primary"
|
color="primary"
|
||||||
i18n
|
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[disabled]="isAgreeButtonDisabled"
|
[disabled]="isAgreeButtonDisabled"
|
||||||
[mat-dialog-close]="data"
|
[mat-dialog-close]="data"
|
||||||
>
|
>
|
||||||
Agree and continue
|
<span i18n>Agree and continue</span>
|
||||||
|
<ion-icon class="ml-1" name="arrow-forward-outline"></ion-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a
|
<a
|
||||||
href="https://money.cnn.com/data/fear-and-greed"
|
href="https://money.cnn.com/data/fear-and-greed/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>Fear & Greed Index →</a
|
>Fear & Greed Index →</a
|
||||||
>
|
>
|
||||||
|
@ -9,7 +9,6 @@ import {
|
|||||||
PortfolioPosition,
|
PortfolioPosition,
|
||||||
User
|
User
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
@ -30,12 +29,12 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
[code: string]: { name: string; value: number };
|
[code: string]: { name: string; value: number };
|
||||||
};
|
};
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
|
public hasImpersonationId: boolean;
|
||||||
public period = 'current';
|
public period = 'current';
|
||||||
public periodOptions: ToggleOption[] = [
|
public periodOptions: ToggleOption[] = [
|
||||||
{ label: 'Initial', value: 'original' },
|
{ label: 'Initial', value: 'original' },
|
||||||
{ label: 'Current', value: 'current' }
|
{ label: 'Current', value: 'current' }
|
||||||
];
|
];
|
||||||
public hasImpersonationId: boolean;
|
|
||||||
public portfolioItems: PortfolioItem[];
|
public portfolioItems: PortfolioItem[];
|
||||||
public portfolioPositions: { [symbol: string]: PortfolioPosition };
|
public portfolioPositions: { [symbol: string]: PortfolioPosition };
|
||||||
public positions: { [symbol: string]: any };
|
public positions: { [symbol: string]: any };
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { PortfolioReportRule } from '@ghostfolio/common/interfaces';
|
import { PortfolioReportRule } from '@ghostfolio/common/interfaces';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
@ -9,7 +9,7 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
templateUrl: './report-page.html',
|
templateUrl: './report-page.html',
|
||||||
styleUrls: ['./report-page.scss']
|
styleUrls: ['./report-page.scss']
|
||||||
})
|
})
|
||||||
export class ReportPageComponent implements OnInit {
|
export class ReportPageComponent implements OnDestroy, OnInit {
|
||||||
public accountClusterRiskRules: PortfolioReportRule[];
|
public accountClusterRiskRules: PortfolioReportRule[];
|
||||||
public currencyClusterRiskRules: PortfolioReportRule[];
|
public currencyClusterRiskRules: PortfolioReportRule[];
|
||||||
public feeRules: PortfolioReportRule[];
|
public feeRules: PortfolioReportRule[];
|
||||||
|
@ -2,7 +2,8 @@ import {
|
|||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
Inject
|
Inject,
|
||||||
|
OnDestroy
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { FormControl, Validators } from '@angular/forms';
|
import { FormControl, Validators } from '@angular/forms';
|
||||||
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
||||||
@ -23,12 +24,12 @@ import { CreateOrUpdateTransactionDialogParams } from './interfaces/interfaces';
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'h-100' },
|
host: { class: 'h-100' },
|
||||||
selector: 'create-or-update-transaction-dialog',
|
selector: 'gf-create-or-update-transaction-dialog',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
styleUrls: ['./create-or-update-transaction-dialog.scss'],
|
styleUrls: ['./create-or-update-transaction-dialog.scss'],
|
||||||
templateUrl: 'create-or-update-transaction-dialog.html'
|
templateUrl: 'create-or-update-transaction-dialog.html'
|
||||||
})
|
})
|
||||||
export class CreateOrUpdateTransactionDialog {
|
export class CreateOrUpdateTransactionDialog implements OnDestroy {
|
||||||
public currencies: Currency[] = [];
|
public currencies: Currency[] = [];
|
||||||
public currentMarketPrice = null;
|
public currentMarketPrice = null;
|
||||||
public filteredLookupItems: Observable<LookupItem[]>;
|
public filteredLookupItems: Observable<LookupItem[]>;
|
||||||
@ -49,10 +50,10 @@ export class CreateOrUpdateTransactionDialog {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.dataService.fetchInfo().subscribe(({ currencies, platforms }) => {
|
const { currencies, platforms } = this.dataService.fetchInfo();
|
||||||
this.currencies = currencies;
|
|
||||||
this.platforms = platforms;
|
this.currencies = currencies;
|
||||||
});
|
this.platforms = platforms;
|
||||||
|
|
||||||
this.filteredLookupItems = this.searchSymbolCtrl.valueChanges.pipe(
|
this.filteredLookupItems = this.searchSymbolCtrl.valueChanges.pipe(
|
||||||
startWith(''),
|
startWith(''),
|
||||||
@ -73,6 +74,7 @@ export class CreateOrUpdateTransactionDialog {
|
|||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(({ marketPrice }) => {
|
.subscribe(({ marketPrice }) => {
|
||||||
this.currentMarketPrice = marketPrice;
|
this.currentMarketPrice = marketPrice;
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,22 @@
|
|||||||
<h1 *ngIf="data.transaction.id" mat-dialog-title i18n>Update transaction</h1>
|
<h1 *ngIf="data.transaction.id" mat-dialog-title i18n>Update transaction</h1>
|
||||||
<h1 *ngIf="!data.transaction.id" mat-dialog-title i18n>Add transaction</h1>
|
<h1 *ngIf="!data.transaction.id" mat-dialog-title i18n>Add transaction</h1>
|
||||||
<div class="flex-grow-1" mat-dialog-content>
|
<div class="flex-grow-1" mat-dialog-content>
|
||||||
|
<div>
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label i18n>Account</mat-label>
|
||||||
|
<mat-select
|
||||||
|
name="accountId"
|
||||||
|
required
|
||||||
|
[(value)]="data.transaction.accountId"
|
||||||
|
>
|
||||||
|
<mat-option
|
||||||
|
*ngFor="let account of data.user?.accounts"
|
||||||
|
[value]="account.id"
|
||||||
|
>{{ account.name }}</mat-option
|
||||||
|
>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
<mat-label i18n>Symbol or ISIN</mat-label>
|
<mat-label i18n>Symbol or ISIN</mat-label>
|
||||||
@ -42,7 +58,7 @@
|
|||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="d-none">
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
<mat-label i18n>Currency</mat-label>
|
<mat-label i18n>Currency</mat-label>
|
||||||
<mat-select
|
<mat-select
|
||||||
@ -136,31 +152,25 @@
|
|||||||
</button>
|
</button>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex" mat-dialog-actions>
|
||||||
|
<gf-value
|
||||||
|
class="flex-grow-1"
|
||||||
|
[currency]="data.transaction.currency"
|
||||||
|
[locale]="data.user?.settings?.locale"
|
||||||
|
[value]="data.transaction.fee + (data.transaction.quantity * data.transaction.unitPrice)"
|
||||||
|
></gf-value>
|
||||||
<div>
|
<div>
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<button i18n mat-button (click)="onCancel()">Cancel</button>
|
||||||
<mat-label i18n>Account</mat-label>
|
<button
|
||||||
<mat-select
|
color="primary"
|
||||||
name="accountId"
|
i18n
|
||||||
required
|
mat-flat-button
|
||||||
[(value)]="data.transaction.accountId"
|
[disabled]="!(addTransactionForm.form.valid && data.transaction.symbol)"
|
||||||
>
|
[mat-dialog-close]="data"
|
||||||
<mat-option *ngFor="let account of data.accounts" [value]="account.id"
|
>
|
||||||
>{{ account.name }}</mat-option
|
Save
|
||||||
>
|
</button>
|
||||||
</mat-select>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="justify-content-end" mat-dialog-actions>
|
|
||||||
<button i18n mat-button (click)="onCancel()">Cancel</button>
|
|
||||||
<button
|
|
||||||
color="primary"
|
|
||||||
i18n
|
|
||||||
mat-flat-button
|
|
||||||
[disabled]="!(addTransactionForm.form.valid && data.transaction.symbol)"
|
|
||||||
[mat-dialog-close]="data"
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
|
@ -9,6 +9,7 @@ import { MatFormFieldModule } from '@angular/material/form-field';
|
|||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
|
import { GfValueModule } from '@ghostfolio/client/components/value/value.module';
|
||||||
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||||
|
|
||||||
import { CreateOrUpdateTransactionDialog } from './create-or-update-transaction-dialog.component';
|
import { CreateOrUpdateTransactionDialog } from './create-or-update-transaction-dialog.component';
|
||||||
@ -19,6 +20,7 @@ import { CreateOrUpdateTransactionDialog } from './create-or-update-transaction-
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
GfSymbolModule,
|
GfSymbolModule,
|
||||||
|
GfValueModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
MatAutocompleteModule,
|
MatAutocompleteModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
|
.mat-dialog-actions {
|
||||||
|
gf-value {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mat-dialog-content {
|
.mat-dialog-content {
|
||||||
max-height: unset;
|
max-height: unset;
|
||||||
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
|
import { User } from '@ghostfolio/common/interfaces';
|
||||||
import { Account, Order } from '@prisma/client';
|
import { Account, Order } from '@prisma/client';
|
||||||
|
|
||||||
export interface CreateOrUpdateTransactionDialogParams {
|
export interface CreateOrUpdateTransactionDialogParams {
|
||||||
accountId: string;
|
accountId: string;
|
||||||
accounts: Account[];
|
|
||||||
transaction: Order;
|
transaction: Order;
|
||||||
|
user: User;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
||||||
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
||||||
@ -9,9 +10,10 @@ import { UserService } from '@ghostfolio/client/services/user/user.service';
|
|||||||
import { User } from '@ghostfolio/common/interfaces';
|
import { User } from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { Order as OrderModel } from '@prisma/client';
|
import { Order as OrderModel } from '@prisma/client';
|
||||||
|
import { format, parseISO } from 'date-fns';
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { Subject, Subscription } from 'rxjs';
|
import { EMPTY, Subject, Subscription } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { catchError, takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
import { CreateOrUpdateTransactionDialog } from './create-or-update-transaction-dialog/create-or-update-transaction-dialog.component';
|
import { CreateOrUpdateTransactionDialog } from './create-or-update-transaction-dialog/create-or-update-transaction-dialog.component';
|
||||||
|
|
||||||
@ -20,11 +22,12 @@ import { CreateOrUpdateTransactionDialog } from './create-or-update-transaction-
|
|||||||
templateUrl: './transactions-page.html',
|
templateUrl: './transactions-page.html',
|
||||||
styleUrls: ['./transactions-page.scss']
|
styleUrls: ['./transactions-page.scss']
|
||||||
})
|
})
|
||||||
export class TransactionsPageComponent implements OnInit {
|
export class TransactionsPageComponent implements OnDestroy, OnInit {
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
public hasImpersonationId: boolean;
|
public hasImpersonationId: boolean;
|
||||||
public hasPermissionToCreateOrder: boolean;
|
public hasPermissionToCreateOrder: boolean;
|
||||||
public hasPermissionToDeleteOrder: boolean;
|
public hasPermissionToDeleteOrder: boolean;
|
||||||
|
public hasPermissionToImportOrders: boolean;
|
||||||
public routeQueryParams: Subscription;
|
public routeQueryParams: Subscription;
|
||||||
public transactions: OrderModel[];
|
public transactions: OrderModel[];
|
||||||
public user: User;
|
public user: User;
|
||||||
@ -42,6 +45,7 @@ export class TransactionsPageComponent implements OnInit {
|
|||||||
private impersonationStorageService: ImpersonationStorageService,
|
private impersonationStorageService: ImpersonationStorageService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
|
private snackBar: MatSnackBar,
|
||||||
private userService: UserService
|
private userService: UserService
|
||||||
) {
|
) {
|
||||||
this.routeQueryParams = route.queryParams
|
this.routeQueryParams = route.queryParams
|
||||||
@ -67,10 +71,18 @@ export class TransactionsPageComponent implements OnInit {
|
|||||||
* Initializes the controller
|
* Initializes the controller
|
||||||
*/
|
*/
|
||||||
public ngOnInit() {
|
public ngOnInit() {
|
||||||
|
const { globalPermissions } = this.dataService.fetchInfo();
|
||||||
|
|
||||||
|
this.hasPermissionToImportOrders = hasPermission(
|
||||||
|
globalPermissions,
|
||||||
|
permissions.enableImport
|
||||||
|
);
|
||||||
|
|
||||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||||
|
|
||||||
this.impersonationStorageService
|
this.impersonationStorageService
|
||||||
.onChangeHasImpersonation()
|
.onChangeHasImpersonation()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((aId) => {
|
.subscribe((aId) => {
|
||||||
this.hasImpersonationId = !!aId;
|
this.hasImpersonationId = !!aId;
|
||||||
});
|
});
|
||||||
@ -98,15 +110,18 @@ export class TransactionsPageComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public fetchOrders() {
|
public fetchOrders() {
|
||||||
this.dataService.fetchOrders().subscribe((response) => {
|
this.dataService
|
||||||
this.transactions = response;
|
.fetchOrders()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((response) => {
|
||||||
|
this.transactions = response;
|
||||||
|
|
||||||
if (this.transactions?.length <= 0) {
|
if (this.transactions?.length <= 0) {
|
||||||
this.router.navigate([], { queryParams: { createDialog: true } });
|
this.router.navigate([], { queryParams: { createDialog: true } });
|
||||||
}
|
}
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onCloneTransaction(aTransaction: OrderModel) {
|
public onCloneTransaction(aTransaction: OrderModel) {
|
||||||
@ -114,11 +129,78 @@ export class TransactionsPageComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onDeleteTransaction(aId: string) {
|
public onDeleteTransaction(aId: string) {
|
||||||
this.dataService.deleteOrder(aId).subscribe({
|
this.dataService
|
||||||
next: () => {
|
.deleteOrder(aId)
|
||||||
this.fetchOrders();
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
}
|
.subscribe({
|
||||||
});
|
next: () => {
|
||||||
|
this.fetchOrders();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public onExport() {
|
||||||
|
this.dataService
|
||||||
|
.fetchExport()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((data) => {
|
||||||
|
this.downloadAsFile(
|
||||||
|
data,
|
||||||
|
`ghostfolio-export-${format(
|
||||||
|
parseISO(data.meta.date),
|
||||||
|
'yyyyMMddHHmm'
|
||||||
|
)}.json`,
|
||||||
|
'text/plain'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public onImport() {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
|
||||||
|
input.onchange = (event) => {
|
||||||
|
// Getting the file reference
|
||||||
|
const file = (event.target as HTMLInputElement).files[0];
|
||||||
|
|
||||||
|
// Setting up the reader
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsText(file, 'UTF-8');
|
||||||
|
|
||||||
|
reader.onload = (readerEvent) => {
|
||||||
|
try {
|
||||||
|
const content = JSON.parse(readerEvent.target.result as string);
|
||||||
|
|
||||||
|
this.snackBar.open('⏳ Importing data...');
|
||||||
|
|
||||||
|
this.dataService
|
||||||
|
.postImport({
|
||||||
|
orders: content.orders
|
||||||
|
})
|
||||||
|
.pipe(
|
||||||
|
catchError((error) => {
|
||||||
|
this.handleImportError(error);
|
||||||
|
|
||||||
|
return EMPTY;
|
||||||
|
}),
|
||||||
|
takeUntil(this.unsubscribeSubject)
|
||||||
|
)
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.fetchOrders();
|
||||||
|
|
||||||
|
this.snackBar.open('✅ Import has been completed', undefined, {
|
||||||
|
duration: 3000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.handleImportError(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
input.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
public onUpdateTransaction(aTransaction: OrderModel) {
|
public onUpdateTransaction(aTransaction: OrderModel) {
|
||||||
@ -141,7 +223,6 @@ export class TransactionsPageComponent implements OnInit {
|
|||||||
}: OrderModel): void {
|
}: OrderModel): void {
|
||||||
const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, {
|
const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, {
|
||||||
data: {
|
data: {
|
||||||
accounts: this.user.accounts,
|
|
||||||
transaction: {
|
transaction: {
|
||||||
accountId,
|
accountId,
|
||||||
currency,
|
currency,
|
||||||
@ -153,25 +234,32 @@ export class TransactionsPageComponent implements OnInit {
|
|||||||
symbol,
|
symbol,
|
||||||
type,
|
type,
|
||||||
unitPrice
|
unitPrice
|
||||||
}
|
},
|
||||||
|
user: this.user
|
||||||
},
|
},
|
||||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||||
});
|
});
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe((data: any) => {
|
dialogRef
|
||||||
const transaction: UpdateOrderDto = data?.transaction;
|
.afterClosed()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((data: any) => {
|
||||||
|
const transaction: UpdateOrderDto = data?.transaction;
|
||||||
|
|
||||||
if (transaction) {
|
if (transaction) {
|
||||||
this.dataService.putOrder(transaction).subscribe({
|
this.dataService
|
||||||
next: () => {
|
.putOrder(transaction)
|
||||||
this.fetchOrders();
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
}
|
.subscribe({
|
||||||
});
|
next: () => {
|
||||||
}
|
this.fetchOrders();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.router.navigate(['.'], { relativeTo: this.route });
|
this.router.navigate(['.'], { relativeTo: this.route });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
@ -179,10 +267,28 @@ export class TransactionsPageComponent implements OnInit {
|
|||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private downloadAsFile(
|
||||||
|
aContent: unknown,
|
||||||
|
aFileName: string,
|
||||||
|
aContentType: string
|
||||||
|
) {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
const file = new Blob([JSON.stringify(aContent, undefined, ' ')], {
|
||||||
|
type: aContentType
|
||||||
|
});
|
||||||
|
a.href = URL.createObjectURL(file);
|
||||||
|
a.download = aFileName;
|
||||||
|
a.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleImportError(aError: unknown) {
|
||||||
|
console.error(aError);
|
||||||
|
this.snackBar.open('❌ Oops, something went wrong...');
|
||||||
|
}
|
||||||
|
|
||||||
private openCreateTransactionDialog(aTransaction?: OrderModel): void {
|
private openCreateTransactionDialog(aTransaction?: OrderModel): void {
|
||||||
const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, {
|
const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, {
|
||||||
data: {
|
data: {
|
||||||
accounts: this.user?.accounts,
|
|
||||||
transaction: {
|
transaction: {
|
||||||
accountId:
|
accountId:
|
||||||
aTransaction?.accountId ??
|
aTransaction?.accountId ??
|
||||||
@ -197,24 +303,28 @@ export class TransactionsPageComponent implements OnInit {
|
|||||||
symbol: aTransaction?.symbol ?? null,
|
symbol: aTransaction?.symbol ?? null,
|
||||||
type: aTransaction?.type ?? 'BUY',
|
type: aTransaction?.type ?? 'BUY',
|
||||||
unitPrice: null
|
unitPrice: null
|
||||||
}
|
},
|
||||||
|
user: this.user
|
||||||
},
|
},
|
||||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||||
});
|
});
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe((data: any) => {
|
dialogRef
|
||||||
const transaction: CreateOrderDto = data?.transaction;
|
.afterClosed()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((data: any) => {
|
||||||
|
const transaction: CreateOrderDto = data?.transaction;
|
||||||
|
|
||||||
if (transaction) {
|
if (transaction) {
|
||||||
this.dataService.postOrder(transaction).subscribe({
|
this.dataService.postOrder(transaction).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.fetchOrders();
|
this.fetchOrders();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.router.navigate(['.'], { relativeTo: this.route });
|
this.router.navigate(['.'], { relativeTo: this.route });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,9 +5,12 @@
|
|||||||
<gf-transactions-table
|
<gf-transactions-table
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
[deviceType]="deviceType"
|
[deviceType]="deviceType"
|
||||||
|
[hasPermissionToImportOrders]="hasPermissionToImportOrders"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[showActions]="!hasImpersonationId && hasPermissionToDeleteOrder"
|
[showActions]="!hasImpersonationId && hasPermissionToDeleteOrder"
|
||||||
[transactions]="transactions"
|
[transactions]="transactions"
|
||||||
|
(export)="onExport()"
|
||||||
|
(import)="onImport()"
|
||||||
(transactionDeleted)="onDeleteTransaction($event)"
|
(transactionDeleted)="onDeleteTransaction($event)"
|
||||||
(transactionToClone)="onCloneTransaction($event)"
|
(transactionToClone)="onCloneTransaction($event)"
|
||||||
(transactionToUpdate)="onUpdateTransaction($event)"
|
(transactionToUpdate)="onUpdateTransaction($event)"
|
||||||
|
@ -1,6 +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 { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { GfTransactionsTableModule } from '@ghostfolio/client/components/transactions-table/transactions-table.module';
|
import { GfTransactionsTableModule } from '@ghostfolio/client/components/transactions-table/transactions-table.module';
|
||||||
|
|
||||||
@ -16,6 +17,7 @@ import { TransactionsPageComponent } from './transactions-page.component';
|
|||||||
CreateOrUpdateTransactionDialogModule,
|
CreateOrUpdateTransactionDialogModule,
|
||||||
GfTransactionsTableModule,
|
GfTransactionsTableModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
|
MatSnackBarModule,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
TransactionsPageRoutingModule
|
TransactionsPageRoutingModule
|
||||||
],
|
],
|
||||||
|
@ -1,16 +1,20 @@
|
|||||||
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||||
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
|
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'gf-webauthn-page',
|
selector: 'gf-webauthn-page',
|
||||||
templateUrl: './webauthn-page.html',
|
templateUrl: './webauthn-page.html',
|
||||||
styleUrls: ['./webauthn-page.scss']
|
styleUrls: ['./webauthn-page.scss']
|
||||||
})
|
})
|
||||||
export class WebauthnPageComponent implements OnInit {
|
export class WebauthnPageComponent implements OnDestroy, OnInit {
|
||||||
public hasError = false;
|
public hasError = false;
|
||||||
|
|
||||||
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
@ -23,24 +27,35 @@ export class WebauthnPageComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public deregisterDevice() {
|
public deregisterDevice() {
|
||||||
this.webAuthnService.deregister().subscribe(() => {
|
this.webAuthnService
|
||||||
this.router.navigate(['/']);
|
.deregister()
|
||||||
});
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.router.navigate(['/']);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public signIn() {
|
public signIn() {
|
||||||
this.hasError = false;
|
this.hasError = false;
|
||||||
|
|
||||||
this.webAuthnService.login().subscribe(
|
this.webAuthnService
|
||||||
({ authToken }) => {
|
.login()
|
||||||
this.tokenStorageService.saveToken(authToken, false);
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
this.router.navigate(['/']);
|
.subscribe(
|
||||||
},
|
({ authToken }) => {
|
||||||
(error) => {
|
this.tokenStorageService.saveToken(authToken, false);
|
||||||
console.error(error);
|
this.router.navigate(['/']);
|
||||||
this.hasError = true;
|
},
|
||||||
this.changeDetectorRef.markForCheck();
|
(error) => {
|
||||||
}
|
console.error(error);
|
||||||
);
|
this.hasError = true;
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ export class ZenPageComponent implements OnDestroy, OnInit {
|
|||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
public hasImpersonationId: boolean;
|
public hasImpersonationId: boolean;
|
||||||
public hasPermissionToReadForeignPortfolio: boolean;
|
public hasPermissionToReadForeignPortfolio: boolean;
|
||||||
|
public hasPositions: boolean;
|
||||||
public historicalDataItems: LineChartItem[];
|
public historicalDataItems: LineChartItem[];
|
||||||
public isLoadingPerformance = true;
|
public isLoadingPerformance = true;
|
||||||
public performance: PortfolioPerformance;
|
public performance: PortfolioPerformance;
|
||||||
@ -61,6 +62,7 @@ export class ZenPageComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
this.impersonationStorageService
|
this.impersonationStorageService
|
||||||
.onChangeHasImpersonation()
|
.onChangeHasImpersonation()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((aId) => {
|
.subscribe((aId) => {
|
||||||
this.hasImpersonationId = !!aId;
|
this.hasImpersonationId = !!aId;
|
||||||
});
|
});
|
||||||
@ -78,6 +80,7 @@ export class ZenPageComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchChart({ range: this.dateRange })
|
.fetchChart({ range: this.dateRange })
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((chartData) => {
|
.subscribe((chartData) => {
|
||||||
this.historicalDataItems = chartData.map((chartDataItem) => {
|
this.historicalDataItems = chartData.map((chartDataItem) => {
|
||||||
return {
|
return {
|
||||||
@ -86,11 +89,14 @@ export class ZenPageComponent implements OnDestroy, OnInit {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.hasPositions = this.historicalDataItems?.length > 0;
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchPortfolioPerformance({ range: this.dateRange })
|
.fetchPortfolioPerformance({ range: this.dateRange })
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((response) => {
|
.subscribe((response) => {
|
||||||
this.performance = response;
|
this.performance = response;
|
||||||
this.isLoadingPerformance = false;
|
this.isLoadingPerformance = false;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div class="container">
|
<div *ngIf="hasPositions || !historicalDataItems" class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="chart-container col mr-3">
|
<div class="chart-container col mr-3">
|
||||||
<gf-line-chart
|
<gf-line-chart
|
||||||
@ -23,3 +23,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
*ngIf="!hasPositions && historicalDataItems"
|
||||||
|
class="d-flex justify-content-center my-5"
|
||||||
|
>
|
||||||
|
<gf-no-transactions-info-indicator></gf-no-transactions-info-indicator>
|
||||||
|
</div>
|
||||||
|
@ -2,6 +2,7 @@ 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 { MatCardModule } from '@angular/material/card';
|
||||||
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
|
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
|
||||||
|
import { GfNoTransactionsInfoModule } from '@ghostfolio/client/components/no-transactions-info/no-transactions-info.module';
|
||||||
import { GfPortfolioPerformanceSummaryModule } from '@ghostfolio/client/components/portfolio-performance-summary/portfolio-performance-summary.module';
|
import { GfPortfolioPerformanceSummaryModule } from '@ghostfolio/client/components/portfolio-performance-summary/portfolio-performance-summary.module';
|
||||||
|
|
||||||
import { ZenPageRoutingModule } from './zen-page-routing.module';
|
import { ZenPageRoutingModule } from './zen-page-routing.module';
|
||||||
@ -13,6 +14,7 @@ import { ZenPageComponent } from './zen-page.component';
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
GfLineChartModule,
|
GfLineChartModule,
|
||||||
|
GfNoTransactionsInfoModule,
|
||||||
GfPortfolioPerformanceSummaryModule,
|
GfPortfolioPerformanceSummaryModule,
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
ZenPageRoutingModule
|
ZenPageRoutingModule
|
||||||
|
@ -2,6 +2,7 @@ import { HttpClient } from '@angular/common/http';
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
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';
|
||||||
|
import { ImportDataDto } from '@ghostfolio/api/app/import/import-data.dto';
|
||||||
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
||||||
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
||||||
import {
|
import {
|
||||||
@ -15,6 +16,7 @@ import { UpdateUserSettingsDto } from '@ghostfolio/api/app/user/update-user-sett
|
|||||||
import {
|
import {
|
||||||
Access,
|
Access,
|
||||||
AdminData,
|
AdminData,
|
||||||
|
Export,
|
||||||
InfoItem,
|
InfoItem,
|
||||||
PortfolioItem,
|
PortfolioItem,
|
||||||
PortfolioOverview,
|
PortfolioOverview,
|
||||||
@ -27,6 +29,7 @@ import { permissions } from '@ghostfolio/common/permissions';
|
|||||||
import { Order as OrderModel } from '@prisma/client';
|
import { Order as OrderModel } from '@prisma/client';
|
||||||
import { Account as AccountModel } from '@prisma/client';
|
import { Account as AccountModel } from '@prisma/client';
|
||||||
import { parseISO } from 'date-fns';
|
import { parseISO } from 'date-fns';
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
@ -86,21 +89,20 @@ export class DataService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchInfo() {
|
public fetchExport() {
|
||||||
return this.http.get<InfoItem>('/api/info').pipe(
|
return this.http.get<Export>('/api/export');
|
||||||
map((data) => {
|
}
|
||||||
if (
|
|
||||||
this.settingsStorageService.getSetting('utm_source') ===
|
|
||||||
'trusted-web-activity'
|
|
||||||
) {
|
|
||||||
data.globalPermissions = data.globalPermissions.filter(
|
|
||||||
(permission) => permission !== permissions.enableSubscription
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
public fetchInfo(): InfoItem {
|
||||||
})
|
const info = cloneDeep((window as any).info);
|
||||||
);
|
|
||||||
|
if (window.localStorage.getItem('utm_source') === 'trusted-web-activity') {
|
||||||
|
info.globalPermissions = info.globalPermissions.filter(
|
||||||
|
(permission) => permission !== permissions.enableSubscription
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchSymbolItem(aSymbol: string) {
|
public fetchSymbolItem(aSymbol: string) {
|
||||||
@ -168,6 +170,10 @@ export class DataService {
|
|||||||
return this.http.post<OrderModel>(`/api/account`, aAccount);
|
return this.http.post<OrderModel>(`/api/account`, aAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public postImport(aImportData: ImportDataDto) {
|
||||||
|
return this.http.post<void>('/api/import', aImportData);
|
||||||
|
}
|
||||||
|
|
||||||
public postOrder(aOrder: CreateOrderDto) {
|
public postOrder(aOrder: CreateOrderDto) {
|
||||||
return this.http.post<OrderModel>(`/api/order`, aOrder);
|
return this.http.post<OrderModel>(`/api/order`, aOrder);
|
||||||
}
|
}
|
||||||
|
4
apps/client/src/assets/robots.txt
Normal file
4
apps/client/src/assets/robots.txt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
Sitemap: https://ghostfol.io/sitemap.xml
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"background_color": "transparent",
|
"background_color": "transparent",
|
||||||
"categories": ["finance", "utilities"],
|
"categories": ["finance", "utilities"],
|
||||||
"description": "Open Source Portfolio Tracker",
|
"description": "Open Source Wealth Management Software",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
export const environment = {
|
export const environment = {
|
||||||
lastPublish: '{BUILD_TIMESTAMP}',
|
lastPublish: '{BUILD_TIMESTAMP}',
|
||||||
production: true,
|
production: true,
|
||||||
stripePublicKey: '{STRIPE_PUBLIC_KEY}',
|
stripePublicKey: '',
|
||||||
version: `v${require('../../../../package.json').version}`
|
version: `v${require('../../../../package.json').version}`
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>Ghostfolio – Open Source Portfolio Tracker</title>
|
<title>Ghostfolio – Open Source Wealth Management Software</title>
|
||||||
<base href="/" />
|
<base href="/" />
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta content="yes" name="apple-mobile-web-app-capable" />
|
<meta content="yes" name="apple-mobile-web-app-capable" />
|
||||||
@ -11,13 +11,13 @@
|
|||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
name="keywords"
|
name="keywords"
|
||||||
content="asset, cryptocurrency, etf, finance, performance, portfolio, stock, tracker, trading"
|
content="app, asset, cryptocurrency, etf, finance, management, performance, portfolio, software, stock, tracker, trading, wealth"
|
||||||
/>
|
/>
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<meta
|
<meta
|
||||||
name="twitter:description"
|
name="twitter:description"
|
||||||
content="Ghostfolio is a lightweight application to keep track of your financial assets like stocks, ETFs or cryptocurrencies across multiple platforms."
|
content="Ghostfolio is a lightweight wealth management application for individuals to keep track of their wealth like stocks, ETFs or cryptocurrencies"
|
||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
name="twitter:image"
|
name="twitter:image"
|
||||||
@ -25,13 +25,13 @@
|
|||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
name="twitter:title"
|
name="twitter:title"
|
||||||
content="Ghostfolio – Open Source Portfolio Tracker"
|
content="Ghostfolio – Open Source Wealth Management Software"
|
||||||
/>
|
/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta property="og:description" content="" />
|
<meta property="og:description" content="" />
|
||||||
<meta
|
<meta
|
||||||
property="og:title"
|
property="og:title"
|
||||||
content="Ghostfolio – Open Source Portfolio Tracker"
|
content="Ghostfolio – Open Source Wealth Management Software"
|
||||||
/>
|
/>
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:url" content="https://www.ghostfol.io" />
|
<meta property="og:url" content="https://www.ghostfol.io" />
|
||||||
@ -42,7 +42,7 @@
|
|||||||
<meta property="og:updated_time" content="2021-03-20T00:00:00+00:00" />
|
<meta property="og:updated_time" content="2021-03-20T00:00:00+00:00" />
|
||||||
<meta
|
<meta
|
||||||
property="og:site_name"
|
property="og:site_name"
|
||||||
content="Ghostfolio – Open Source Portfolio Tracker"
|
content="Ghostfolio – Open Source Wealth Management Software"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<link
|
<link
|
||||||
|
@ -1,16 +1,33 @@
|
|||||||
import { enableProdMode } from '@angular/core';
|
import { enableProdMode } from '@angular/core';
|
||||||
import { LOCALE_ID } from '@angular/core';
|
import { LOCALE_ID } from '@angular/core';
|
||||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||||
|
import { InfoItem } from '@ghostfolio/common/interfaces';
|
||||||
|
import { permissions } from '@ghostfolio/common/permissions';
|
||||||
|
|
||||||
import { AppModule } from './app/app.module';
|
import { AppModule } from './app/app.module';
|
||||||
import { environment } from './environments/environment';
|
import { environment } from './environments/environment';
|
||||||
|
|
||||||
if (environment.production) {
|
(async () => {
|
||||||
enableProdMode();
|
const response = await fetch('/api/info');
|
||||||
}
|
const info: InfoItem = await response.json();
|
||||||
|
|
||||||
platformBrowserDynamic()
|
if (window.localStorage.getItem('utm_source') === 'trusted-web-activity') {
|
||||||
.bootstrapModule(AppModule, {
|
info.globalPermissions = info.globalPermissions.filter(
|
||||||
providers: [{ provide: LOCALE_ID, useValue: 'de-CH' }]
|
(permission) => permission !== permissions.enableSubscription
|
||||||
})
|
);
|
||||||
.catch((err) => console.error(err));
|
}
|
||||||
|
|
||||||
|
(window as any).info = info;
|
||||||
|
|
||||||
|
environment.stripePublicKey = info.stripePublicKey;
|
||||||
|
|
||||||
|
if (environment.production) {
|
||||||
|
enableProdMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
platformBrowserDynamic()
|
||||||
|
.bootstrapModule(AppModule, {
|
||||||
|
providers: [{ provide: LOCALE_ID, useValue: 'de-CH' }]
|
||||||
|
})
|
||||||
|
.catch((err) => console.error(err));
|
||||||
|
})();
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user