Compare commits
38 Commits
Author | SHA1 | Date | |
---|---|---|---|
c1c22c195d | |||
111d8d8e3c | |||
b0a24e4fc0 | |||
694b9b8991 | |||
fada347aa5 | |||
f37ea9f0e7 | |||
59911925c2 | |||
58fd59beb1 | |||
eb09d77251 | |||
42b9178d96 | |||
45516311f5 | |||
04cfa7366f | |||
4234ab84a9 | |||
b8c05d1014 | |||
91ec9aa0a4 | |||
565e920f1b | |||
5d24adfa75 | |||
1dc94c0027 | |||
ebae2f4ec9 | |||
7099edc591 | |||
de973d6bda | |||
993a491d24 | |||
631efff7ae | |||
a3d1ac2ce4 | |||
4484c21757 | |||
87cd3ef33f | |||
163f4a3d3f | |||
a84256dc03 | |||
cf82066976 | |||
e248c9cedd | |||
90a2fea7d6 | |||
d17b02092e | |||
c70eb7793e | |||
e3a1d2b9cf | |||
e68ebdf76d | |||
949cf58eef | |||
0816defb95 | |||
a076a1c933 |
9
.travis.yml
Normal file
9
.travis.yml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
language: node_js
|
||||||
|
git:
|
||||||
|
depth: false
|
||||||
|
node_js:
|
||||||
|
- 14
|
||||||
|
before_script:
|
||||||
|
- yarn
|
||||||
|
script:
|
||||||
|
- yarn format:check
|
91
CHANGELOG.md
91
CHANGELOG.md
@ -5,6 +5,97 @@ 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.3.0 - 15.05.2021
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Refactored the active menu item state by parsing the current url
|
||||||
|
- Used a desaturated background color for unknown types in pie charts
|
||||||
|
- Renamed the columns _Initial Share_ and _Current Share_ to _Initial Allocation_ and _Current Allocation_ in the positions table
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the link to the pricing page
|
||||||
|
|
||||||
|
## 1.2.1 - 14.05.2021
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Updated the sitemap
|
||||||
|
|
||||||
|
## 1.2.0 - 14.05.2021
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Harmonized the style of various tables
|
||||||
|
- Keep the color per type when switching between _Initial_ and _Current_ in pie charts
|
||||||
|
- Upgraded `chart.js` from version `3.0.2` to `3.2.1`
|
||||||
|
- Moved the pricing section to a dedicated page
|
||||||
|
- Improved the style of the transaction filtering component
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the tooltips when switching between _Initial_ and _Current_ in pie charts
|
||||||
|
|
||||||
|
## 1.1.0 - 11.05.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added a button to fetch the current market price in the create or edit transaction dialog
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the transaction filtering with multi filter support
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the filtering by account name in the transactions table
|
||||||
|
- Fixed the active menu item state when a modal has opened
|
||||||
|
|
||||||
|
## 1.0.0 - 05.05.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the functionality to clone a transaction
|
||||||
|
- Added a _Google Play_ badge on the landing page
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Changed to maskable icons
|
||||||
|
|
||||||
|
## 0.99.0 - 03.05.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support for deleting users in the admin control panel
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Eliminated the platform attribute from the transaction model
|
||||||
|
|
||||||
|
## 0.98.0 - 02.05.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the logic to create and update accounts
|
||||||
|
|
||||||
|
## 0.97.0 - 01.05.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added an account page as a preparation for the multi accounts support
|
||||||
|
|
||||||
|
## 0.96.0 - 30.04.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the absolute change to the position detail dialog
|
||||||
|
- Added the number of transactions to the position detail dialog
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Harmonized the slogan to "Open Source Portfolio Tracker"
|
||||||
|
|
||||||
## 0.95.0 - 28.04.2021
|
## 0.95.0 - 28.04.2021
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
24
README.md
24
README.md
@ -7,8 +7,11 @@
|
|||||||
<a href="https://ghostfol.io"><strong>Live Demo</strong></a>
|
<a href="https://ghostfol.io"><strong>Live Demo</strong></a>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
|
<a href="https://travis-ci.org/github/ghostfolio/ghostfolio" rel="nofollow">
|
||||||
|
<img src="https://travis-ci.org/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">
|
||||||
<img src="https://img.shields.io/badge/License-AGPL%20v3-blue.svg" alt="License: AGPL v3">
|
<img src="https://img.shields.io/badge/License-AGPL%20v3-blue.svg" alt="License: AGPL v3"/>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -68,19 +71,18 @@ The frontend is built with [Angular](https://angular.io).
|
|||||||
### Setup
|
### Setup
|
||||||
|
|
||||||
1. Run `yarn install`
|
1. Run `yarn install`
|
||||||
2. Run `cd docker`
|
1. Run `cd docker`
|
||||||
3. Run `docker compose build`
|
1. Run `docker compose up -d` to start [PostgreSQL](https://www.postgresql.org) and [Redis](https://redis.io)
|
||||||
4. Run `docker compose up -d` to start [PostgreSQL](https://www.postgresql.org) and [Redis](https://redis.io)
|
1. Run `cd -` to go back to the project root directory
|
||||||
5. Run `cd -` to go back to the project root directory
|
1. Run `yarn setup:database` to initialize the database schema and populate your database with (example) data
|
||||||
6. Run `yarn setup:database` to initialize the database schema and populate your database with (example) data
|
1. Start server and client (see [_Development_](#Development))
|
||||||
7. Start server and client (see _Development_)
|
1. Login as _Admin_ with the following _Security Token_: `ae76872ae8f3419c6d6f64bf51888ecbcc703927a342d815fafe486acdb938da07d0cf44fca211a0be74a423238f535362d390a41e81e633a9ce668a6e31cdf9`
|
||||||
8. 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
|
||||||
9. Go to the _Admin Control Panel_ and press _Gather All Data_ to fetch historical data
|
1. Press _Sign out_ and check out the _Live Demo_
|
||||||
10. Press _Sign out_ and check out the _Live Demo_
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
Please make sure you have completed the instructions from _Setup_
|
Please make sure you have completed the instructions from [_Setup_](#Setup)
|
||||||
|
|
||||||
### Start server
|
### Start server
|
||||||
|
|
||||||
|
237
apps/api/src/app/account/account.controller.ts
Normal file
237
apps/api/src/app/account/account.controller.ts
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
import { RequestWithUser } from '@ghostfolio/api/app/interfaces/request-with-user.type';
|
||||||
|
import { nullifyValuesInObjects } from '@ghostfolio/api/helper/object.helper';
|
||||||
|
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
||||||
|
import { getPermissions, hasPermission, permissions } from '@ghostfolio/helper';
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
Headers,
|
||||||
|
HttpException,
|
||||||
|
Inject,
|
||||||
|
Param,
|
||||||
|
Post,
|
||||||
|
Put,
|
||||||
|
UseGuards
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { REQUEST } from '@nestjs/core';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { Account as AccountModel, Order } from '@prisma/client';
|
||||||
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
|
||||||
|
import { AccountService } from './account.service';
|
||||||
|
import { CreateAccountDto } from './create-account.dto';
|
||||||
|
import { UpdateAccountDto } from './update-account.dto';
|
||||||
|
|
||||||
|
@Controller('account')
|
||||||
|
export class AccountController {
|
||||||
|
public constructor(
|
||||||
|
private readonly accountService: AccountService,
|
||||||
|
private readonly impersonationService: ImpersonationService,
|
||||||
|
@Inject(REQUEST) private readonly request: RequestWithUser
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
public async deleteAccount(@Param('id') id: string): Promise<AccountModel> {
|
||||||
|
if (
|
||||||
|
!hasPermission(
|
||||||
|
getPermissions(this.request.user.role),
|
||||||
|
permissions.deleteAccount
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
|
StatusCodes.FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const account = await this.accountService.accountWithOrders(
|
||||||
|
{
|
||||||
|
id_userId: {
|
||||||
|
id,
|
||||||
|
userId: this.request.user.id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ Order: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (account?.isDefault || account?.Order.length > 0) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
|
StatusCodes.FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.accountService.deleteAccount(
|
||||||
|
{
|
||||||
|
id_userId: {
|
||||||
|
id,
|
||||||
|
userId: this.request.user.id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
this.request.user.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
public async getAllAccounts(
|
||||||
|
@Headers('impersonation-id') impersonationId
|
||||||
|
): Promise<AccountModel[]> {
|
||||||
|
const impersonationUserId = await this.impersonationService.validateImpersonationId(
|
||||||
|
impersonationId,
|
||||||
|
this.request.user.id
|
||||||
|
);
|
||||||
|
|
||||||
|
let accounts = await this.accountService.accounts({
|
||||||
|
include: { Order: true, Platform: true },
|
||||||
|
orderBy: { name: 'asc' },
|
||||||
|
where: { userId: impersonationUserId || this.request.user.id }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
impersonationUserId &&
|
||||||
|
!hasPermission(
|
||||||
|
getPermissions(this.request.user.role),
|
||||||
|
permissions.readForeignPortfolio
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
accounts = nullifyValuesInObjects(accounts, [
|
||||||
|
'fee',
|
||||||
|
'quantity',
|
||||||
|
'unitPrice'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return accounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
public async getAccountById(@Param('id') id: string): Promise<AccountModel> {
|
||||||
|
return this.accountService.account({
|
||||||
|
id_userId: {
|
||||||
|
id,
|
||||||
|
userId: this.request.user.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
public async createAccount(
|
||||||
|
@Body() data: CreateAccountDto
|
||||||
|
): Promise<AccountModel> {
|
||||||
|
if (
|
||||||
|
!hasPermission(
|
||||||
|
getPermissions(this.request.user.role),
|
||||||
|
permissions.createAccount
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
|
StatusCodes.FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.platformId) {
|
||||||
|
const platformId = data.platformId;
|
||||||
|
delete data.platformId;
|
||||||
|
|
||||||
|
return this.accountService.createAccount(
|
||||||
|
{
|
||||||
|
...data,
|
||||||
|
Platform: { connect: { id: platformId } },
|
||||||
|
User: { connect: { id: this.request.user.id } }
|
||||||
|
},
|
||||||
|
this.request.user.id
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
delete data.platformId;
|
||||||
|
|
||||||
|
return this.accountService.createAccount(
|
||||||
|
{
|
||||||
|
...data,
|
||||||
|
User: { connect: { id: this.request.user.id } }
|
||||||
|
},
|
||||||
|
this.request.user.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put(':id')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
public async update(@Param('id') id: string, @Body() data: UpdateAccountDto) {
|
||||||
|
if (
|
||||||
|
!hasPermission(
|
||||||
|
getPermissions(this.request.user.role),
|
||||||
|
permissions.updateAccount
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
|
StatusCodes.FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalAccount = await this.accountService.account({
|
||||||
|
id_userId: {
|
||||||
|
id,
|
||||||
|
userId: this.request.user.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!originalAccount) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
|
StatusCodes.FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.platformId) {
|
||||||
|
const platformId = data.platformId;
|
||||||
|
delete data.platformId;
|
||||||
|
|
||||||
|
return this.accountService.updateAccount(
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
...data,
|
||||||
|
Platform: { connect: { id: platformId } },
|
||||||
|
User: { connect: { id: this.request.user.id } }
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id_userId: {
|
||||||
|
id,
|
||||||
|
userId: this.request.user.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
this.request.user.id
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// platformId is null, remove it
|
||||||
|
delete data.platformId;
|
||||||
|
|
||||||
|
return this.accountService.updateAccount(
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
...data,
|
||||||
|
Platform: originalAccount.platformId
|
||||||
|
? { disconnect: true }
|
||||||
|
: undefined,
|
||||||
|
User: { connect: { id: this.request.user.id } }
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id_userId: {
|
||||||
|
id,
|
||||||
|
userId: this.request.user.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
this.request.user.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
apps/api/src/app/account/account.module.ts
Normal file
30
apps/api/src/app/account/account.module.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration.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 { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
||||||
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { RedisCacheModule } from '../redis-cache/redis-cache.module';
|
||||||
|
import { AccountController } from './account.controller';
|
||||||
|
import { AccountService } from './account.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [RedisCacheModule],
|
||||||
|
controllers: [AccountController],
|
||||||
|
providers: [
|
||||||
|
AccountService,
|
||||||
|
AlphaVantageService,
|
||||||
|
ConfigurationService,
|
||||||
|
DataProviderService,
|
||||||
|
GhostfolioScraperApiService,
|
||||||
|
ImpersonationService,
|
||||||
|
PrismaService,
|
||||||
|
RakutenRapidApiService,
|
||||||
|
YahooFinanceService
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AccountModule {}
|
89
apps/api/src/app/account/account.service.ts
Normal file
89
apps/api/src/app/account/account.service.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Account, Order, Prisma } from '@prisma/client';
|
||||||
|
|
||||||
|
import { RedisCacheService } from '../redis-cache/redis-cache.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AccountService {
|
||||||
|
public constructor(
|
||||||
|
private readonly redisCacheService: RedisCacheService,
|
||||||
|
private prisma: PrismaService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async account(
|
||||||
|
accountWhereUniqueInput: Prisma.AccountWhereUniqueInput
|
||||||
|
): Promise<Account | null> {
|
||||||
|
return this.prisma.account.findUnique({
|
||||||
|
where: accountWhereUniqueInput
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async accountWithOrders(
|
||||||
|
accountWhereUniqueInput: Prisma.AccountWhereUniqueInput,
|
||||||
|
accountInclude: Prisma.AccountInclude
|
||||||
|
): Promise<
|
||||||
|
Account & {
|
||||||
|
Order?: Order[];
|
||||||
|
}
|
||||||
|
> {
|
||||||
|
return this.prisma.account.findUnique({
|
||||||
|
include: accountInclude,
|
||||||
|
where: accountWhereUniqueInput
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async accounts(params: {
|
||||||
|
include?: Prisma.AccountInclude;
|
||||||
|
skip?: number;
|
||||||
|
take?: number;
|
||||||
|
cursor?: Prisma.AccountWhereUniqueInput;
|
||||||
|
where?: Prisma.AccountWhereInput;
|
||||||
|
orderBy?: Prisma.AccountOrderByInput;
|
||||||
|
}): Promise<Account[]> {
|
||||||
|
const { include, skip, take, cursor, where, orderBy } = params;
|
||||||
|
|
||||||
|
return this.prisma.account.findMany({
|
||||||
|
cursor,
|
||||||
|
include,
|
||||||
|
orderBy,
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
where
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createAccount(
|
||||||
|
data: Prisma.AccountCreateInput,
|
||||||
|
aUserId: string
|
||||||
|
): Promise<Account> {
|
||||||
|
return this.prisma.account.create({
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteAccount(
|
||||||
|
where: Prisma.AccountWhereUniqueInput,
|
||||||
|
aUserId: string
|
||||||
|
): Promise<Account> {
|
||||||
|
this.redisCacheService.remove(`${aUserId}.portfolio`);
|
||||||
|
|
||||||
|
return this.prisma.account.delete({
|
||||||
|
where
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateAccount(
|
||||||
|
params: {
|
||||||
|
where: Prisma.AccountWhereUniqueInput;
|
||||||
|
data: Prisma.AccountUpdateInput;
|
||||||
|
},
|
||||||
|
aUserId: string
|
||||||
|
): Promise<Account> {
|
||||||
|
const { data, where } = params;
|
||||||
|
return this.prisma.account.update({
|
||||||
|
data,
|
||||||
|
where
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
14
apps/api/src/app/account/create-account.dto.ts
Normal file
14
apps/api/src/app/account/create-account.dto.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { AccountType } from '@prisma/client';
|
||||||
|
import { IsString, ValidateIf } from 'class-validator';
|
||||||
|
|
||||||
|
export class CreateAccountDto {
|
||||||
|
@IsString()
|
||||||
|
accountType: AccountType;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@ValidateIf((object, value) => value !== null)
|
||||||
|
platformId: string | null;
|
||||||
|
}
|
17
apps/api/src/app/account/update-account.dto.ts
Normal file
17
apps/api/src/app/account/update-account.dto.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { AccountType } from '@prisma/client';
|
||||||
|
import { IsString, ValidateIf } from 'class-validator';
|
||||||
|
|
||||||
|
export class UpdateAccountDto {
|
||||||
|
@IsString()
|
||||||
|
accountType: AccountType;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@ValidateIf((object, value) => value !== null)
|
||||||
|
platformId: string | null;
|
||||||
|
}
|
@ -16,6 +16,7 @@ import { YahooFinanceService } from '../services/data-provider/yahoo-finance/yah
|
|||||||
import { ExchangeRateDataService } from '../services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '../services/exchange-rate-data.service';
|
||||||
import { PrismaService } from '../services/prisma.service';
|
import { PrismaService } from '../services/prisma.service';
|
||||||
import { AccessModule } from './access/access.module';
|
import { AccessModule } from './access/access.module';
|
||||||
|
import { AccountModule } from './account/account.module';
|
||||||
import { AdminModule } from './admin/admin.module';
|
import { AdminModule } from './admin/admin.module';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AuthModule } from './auth/auth.module';
|
import { AuthModule } from './auth/auth.module';
|
||||||
@ -32,6 +33,7 @@ import { UserModule } from './user/user.module';
|
|||||||
imports: [
|
imports: [
|
||||||
AdminModule,
|
AdminModule,
|
||||||
AccessModule,
|
AccessModule,
|
||||||
|
AccountModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
CacheModule,
|
CacheModule,
|
||||||
ConfigModule.forRoot(),
|
ConfigModule.forRoot(),
|
||||||
|
@ -7,7 +7,7 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { Currency, Type } from '@prisma/client';
|
import { Currency, Type } from '@prisma/client';
|
||||||
import { parseISO } from 'date-fns';
|
import { parseISO } from 'date-fns';
|
||||||
|
|
||||||
import { OrderWithPlatform } from '../order/interfaces/order-with-platform.type';
|
import { OrderWithAccount } from '../order/interfaces/order-with-account.type';
|
||||||
import { CreateOrderDto } from './create-order.dto';
|
import { CreateOrderDto } from './create-order.dto';
|
||||||
import { Data } from './interfaces/data.interface';
|
import { Data } from './interfaces/data.interface';
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ export class ExperimentalService {
|
|||||||
aDate: Date,
|
aDate: Date,
|
||||||
aBaseCurrency: Currency
|
aBaseCurrency: Currency
|
||||||
): Promise<Data> {
|
): Promise<Data> {
|
||||||
const ordersWithPlatform: OrderWithPlatform[] = aOrders.map((order) => {
|
const ordersWithPlatform: OrderWithAccount[] = aOrders.map((order) => {
|
||||||
return {
|
return {
|
||||||
...order,
|
...order,
|
||||||
accountId: undefined,
|
accountId: undefined,
|
||||||
|
@ -17,10 +17,6 @@ export class CreateOrderDto {
|
|||||||
@IsNumber()
|
@IsNumber()
|
||||||
fee: number;
|
fee: number;
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@ValidateIf((object, value) => value !== null)
|
|
||||||
platformId: string | null;
|
|
||||||
|
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
import { Account, Order, Platform } from '@prisma/client';
|
||||||
|
|
||||||
|
type AccountWithPlatform = Account & { Platform?: Platform };
|
||||||
|
|
||||||
|
export type OrderWithAccount = Order & { Account?: AccountWithPlatform };
|
@ -1,3 +0,0 @@
|
|||||||
import { Order, Platform } from '@prisma/client';
|
|
||||||
|
|
||||||
export type OrderWithPlatform = Order & { Platform?: Platform };
|
|
@ -71,7 +71,11 @@ export class OrderController {
|
|||||||
|
|
||||||
let orders = await this.orderService.orders({
|
let orders = await this.orderService.orders({
|
||||||
include: {
|
include: {
|
||||||
Platform: true
|
Account: {
|
||||||
|
include: {
|
||||||
|
Platform: true
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
orderBy: { date: 'desc' },
|
orderBy: { date: 'desc' },
|
||||||
where: { userId: impersonationUserId || this.request.user.id }
|
where: { userId: impersonationUserId || this.request.user.id }
|
||||||
@ -121,41 +125,19 @@ export class OrderController {
|
|||||||
const accountId = data.accountId;
|
const accountId = data.accountId;
|
||||||
delete data.accountId;
|
delete data.accountId;
|
||||||
|
|
||||||
if (data.platformId) {
|
return this.orderService.createOrder(
|
||||||
const platformId = data.platformId;
|
{
|
||||||
delete data.platformId;
|
...data,
|
||||||
|
date,
|
||||||
return this.orderService.createOrder(
|
Account: {
|
||||||
{
|
connect: {
|
||||||
...data,
|
id_userId: { id: accountId, userId: this.request.user.id }
|
||||||
date,
|
}
|
||||||
Account: {
|
|
||||||
connect: {
|
|
||||||
id_userId: { id: accountId, userId: this.request.user.id }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Platform: { connect: { id: platformId } },
|
|
||||||
User: { connect: { id: this.request.user.id } }
|
|
||||||
},
|
},
|
||||||
this.request.user.id
|
User: { connect: { id: this.request.user.id } }
|
||||||
);
|
},
|
||||||
} else {
|
this.request.user.id
|
||||||
delete data.platformId;
|
);
|
||||||
|
|
||||||
return this.orderService.createOrder(
|
|
||||||
{
|
|
||||||
...data,
|
|
||||||
date,
|
|
||||||
Account: {
|
|
||||||
connect: {
|
|
||||||
id_userId: { id: accountId, userId: this.request.user.id }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
User: { connect: { id: this.request.user.id } }
|
|
||||||
},
|
|
||||||
this.request.user.id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put(':id')
|
@Put(':id')
|
||||||
@ -180,65 +162,38 @@ export class OrderController {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!originalOrder) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
|
StatusCodes.FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const date = parseISO(data.date);
|
const date = parseISO(data.date);
|
||||||
|
|
||||||
const accountId = data.accountId;
|
const accountId = data.accountId;
|
||||||
delete data.accountId;
|
delete data.accountId;
|
||||||
|
|
||||||
if (data.platformId) {
|
return this.orderService.updateOrder(
|
||||||
const platformId = data.platformId;
|
{
|
||||||
delete data.platformId;
|
data: {
|
||||||
|
...data,
|
||||||
return this.orderService.updateOrder(
|
date,
|
||||||
{
|
Account: {
|
||||||
data: {
|
connect: {
|
||||||
...data,
|
id_userId: { id: accountId, userId: this.request.user.id }
|
||||||
date,
|
|
||||||
Account: {
|
|
||||||
connect: {
|
|
||||||
id_userId: { id: accountId, userId: this.request.user.id }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Platform: { connect: { id: platformId } },
|
|
||||||
User: { connect: { id: this.request.user.id } }
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
id_userId: {
|
|
||||||
id,
|
|
||||||
userId: this.request.user.id
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
this.request.user.id
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// platformId is null, remove it
|
|
||||||
delete data.platformId;
|
|
||||||
|
|
||||||
return this.orderService.updateOrder(
|
|
||||||
{
|
|
||||||
data: {
|
|
||||||
...data,
|
|
||||||
date,
|
|
||||||
Account: {
|
|
||||||
connect: {
|
|
||||||
id_userId: { id: accountId, userId: this.request.user.id }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Platform: originalOrder.platformId
|
|
||||||
? { disconnect: true }
|
|
||||||
: undefined,
|
|
||||||
User: { connect: { id: this.request.user.id } }
|
|
||||||
},
|
},
|
||||||
where: {
|
User: { connect: { id: this.request.user.id } }
|
||||||
id_userId: {
|
|
||||||
id,
|
|
||||||
userId: this.request.user.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
this.request.user.id
|
where: {
|
||||||
);
|
id_userId: {
|
||||||
}
|
id,
|
||||||
|
userId: this.request.user.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
this.request.user.id
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import { Order, Prisma } from '@prisma/client';
|
|||||||
|
|
||||||
import { CacheService } from '../cache/cache.service';
|
import { CacheService } from '../cache/cache.service';
|
||||||
import { RedisCacheService } from '../redis-cache/redis-cache.service';
|
import { RedisCacheService } from '../redis-cache/redis-cache.service';
|
||||||
import { OrderWithPlatform } from './interfaces/order-with-platform.type';
|
import { OrderWithAccount } from './interfaces/order-with-account.type';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OrderService {
|
export class OrderService {
|
||||||
@ -31,7 +31,7 @@ export class OrderService {
|
|||||||
cursor?: Prisma.OrderWhereUniqueInput;
|
cursor?: Prisma.OrderWhereUniqueInput;
|
||||||
where?: Prisma.OrderWhereInput;
|
where?: Prisma.OrderWhereInput;
|
||||||
orderBy?: Prisma.OrderOrderByInput;
|
orderBy?: Prisma.OrderOrderByInput;
|
||||||
}): Promise<OrderWithPlatform[]> {
|
}): Promise<OrderWithAccount[]> {
|
||||||
const { include, skip, take, cursor, where, orderBy } = params;
|
const { include, skip, take, cursor, where, orderBy } = params;
|
||||||
|
|
||||||
return this.prisma.order.findMany({
|
return this.prisma.order.findMany({
|
||||||
|
@ -17,10 +17,6 @@ export class UpdateOrderDto {
|
|||||||
@IsNumber()
|
@IsNumber()
|
||||||
fee: number;
|
fee: number;
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@ValidateIf((object, value) => value !== null)
|
|
||||||
platformId: string | null;
|
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
|
@ -16,4 +16,5 @@ export interface Position {
|
|||||||
investmentInOriginalCurrency?: number;
|
investmentInOriginalCurrency?: number;
|
||||||
marketPrice?: number;
|
marketPrice?: number;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
transactionCount: number;
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ export interface PortfolioPositionDetail {
|
|||||||
minPrice: number;
|
minPrice: number;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
|
transactionCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HistoricalDataItem {
|
export interface HistoricalDataItem {
|
||||||
|
@ -2,6 +2,11 @@ import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces';
|
|||||||
import { Currency } from '@prisma/client';
|
import { Currency } from '@prisma/client';
|
||||||
|
|
||||||
export interface PortfolioPosition {
|
export interface PortfolioPosition {
|
||||||
|
accounts: {
|
||||||
|
[name: string]: { current: number; original: number };
|
||||||
|
};
|
||||||
|
allocationCurrent: number;
|
||||||
|
allocationInvestment: number;
|
||||||
currency: Currency;
|
currency: Currency;
|
||||||
exchange?: string;
|
exchange?: string;
|
||||||
grossPerformance: number;
|
grossPerformance: number;
|
||||||
@ -13,13 +18,9 @@ export interface PortfolioPosition {
|
|||||||
marketPrice: number;
|
marketPrice: number;
|
||||||
marketState: MarketState;
|
marketState: MarketState;
|
||||||
name: string;
|
name: string;
|
||||||
platforms: {
|
|
||||||
[name: string]: { current: number; original: number };
|
|
||||||
};
|
|
||||||
quantity: number;
|
quantity: number;
|
||||||
sector?: string;
|
sector?: string;
|
||||||
shareCurrent: number;
|
transactionCount: number;
|
||||||
shareInvestment: number;
|
|
||||||
symbol: string;
|
symbol: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
|
@ -185,11 +185,11 @@ export class PortfolioController {
|
|||||||
portfolioPosition.investment =
|
portfolioPosition.investment =
|
||||||
portfolioPosition.investment / totalInvestment;
|
portfolioPosition.investment / totalInvestment;
|
||||||
|
|
||||||
for (const [platform, { current, original }] of Object.entries(
|
for (const [account, { current, original }] of Object.entries(
|
||||||
portfolioPosition.platforms
|
portfolioPosition.accounts
|
||||||
)) {
|
)) {
|
||||||
portfolioPosition.platforms[platform].current = current / totalValue;
|
portfolioPosition.accounts[account].current = current / totalValue;
|
||||||
portfolioPosition.platforms[platform].original =
|
portfolioPosition.accounts[account].original =
|
||||||
original / totalInvestment;
|
original / totalInvestment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ export class PortfolioService {
|
|||||||
// Get portfolio from database
|
// Get portfolio from database
|
||||||
const orders = await this.orderService.orders({
|
const orders = await this.orderService.orders({
|
||||||
include: {
|
include: {
|
||||||
Platform: true
|
Account: true
|
||||||
},
|
},
|
||||||
orderBy: { date: 'asc' },
|
orderBy: { date: 'asc' },
|
||||||
where: { userId: aUserId }
|
where: { userId: aUserId }
|
||||||
@ -209,7 +209,8 @@ export class PortfolioService {
|
|||||||
firstBuyDate,
|
firstBuyDate,
|
||||||
investment,
|
investment,
|
||||||
marketPrice,
|
marketPrice,
|
||||||
quantity
|
quantity,
|
||||||
|
transactionCount
|
||||||
} = portfolio.getPositions(new Date())[aSymbol];
|
} = portfolio.getPositions(new Date())[aSymbol];
|
||||||
|
|
||||||
const historicalData = await this.dataProviderService.getHistorical(
|
const historicalData = await this.dataProviderService.getHistorical(
|
||||||
@ -262,6 +263,7 @@ export class PortfolioService {
|
|||||||
maxPrice,
|
maxPrice,
|
||||||
minPrice,
|
minPrice,
|
||||||
quantity,
|
quantity,
|
||||||
|
transactionCount,
|
||||||
grossPerformance: this.exchangeRateDataService.toCurrency(
|
grossPerformance: this.exchangeRateDataService.toCurrency(
|
||||||
marketPrice - averagePrice,
|
marketPrice - averagePrice,
|
||||||
currency,
|
currency,
|
||||||
@ -315,7 +317,8 @@ export class PortfolioService {
|
|||||||
maxPrice: undefined,
|
maxPrice: undefined,
|
||||||
minPrice: undefined,
|
minPrice: undefined,
|
||||||
quantity: undefined,
|
quantity: undefined,
|
||||||
symbol: aSymbol
|
symbol: aSymbol,
|
||||||
|
transactionCount: undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -331,7 +334,8 @@ export class PortfolioService {
|
|||||||
maxPrice: undefined,
|
maxPrice: undefined,
|
||||||
minPrice: undefined,
|
minPrice: undefined,
|
||||||
quantity: undefined,
|
quantity: undefined,
|
||||||
symbol: aSymbol
|
symbol: aSymbol,
|
||||||
|
transactionCount: undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import { getPermissions, hasPermission, permissions } from '@ghostfolio/helper';
|
|||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
|
Delete,
|
||||||
Get,
|
Get,
|
||||||
HttpException,
|
HttpException,
|
||||||
Inject,
|
Inject,
|
||||||
@ -15,6 +16,7 @@ import { REQUEST } from '@nestjs/core';
|
|||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { Provider } from '@prisma/client';
|
import { Provider } from '@prisma/client';
|
||||||
|
import { User as UserModel } from '@prisma/client';
|
||||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
|
||||||
import { UserItem } from './interfaces/user-item.interface';
|
import { UserItem } from './interfaces/user-item.interface';
|
||||||
@ -30,6 +32,27 @@ export class UserController {
|
|||||||
private readonly userService: UserService
|
private readonly userService: UserService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
public async deleteUser(@Param('id') id: string): Promise<UserModel> {
|
||||||
|
if (
|
||||||
|
!hasPermission(
|
||||||
|
getPermissions(this.request.user.role),
|
||||||
|
permissions.deleteUser
|
||||||
|
) ||
|
||||||
|
id === this.request.user.id
|
||||||
|
) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
|
StatusCodes.FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.userService.deleteUser({
|
||||||
|
id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
public async getUser(@Param('id') id: string): Promise<User> {
|
public async getUser(@Param('id') id: string): Promise<User> {
|
||||||
|
@ -163,6 +163,28 @@ export class UserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async deleteUser(where: Prisma.UserWhereUniqueInput): Promise<User> {
|
public async deleteUser(where: Prisma.UserWhereUniqueInput): Promise<User> {
|
||||||
|
await this.prisma.access.deleteMany({
|
||||||
|
where: { OR: [{ granteeUserId: where.id }, { userId: where.id }] }
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.prisma.account.deleteMany({
|
||||||
|
where: { userId: where.id }
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.prisma.analytics.delete({
|
||||||
|
where: { userId: where.id }
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.prisma.order.deleteMany({
|
||||||
|
where: { userId: where.id }
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.prisma.settings.delete({
|
||||||
|
where: { userId: where.id }
|
||||||
|
});
|
||||||
|
} catch {}
|
||||||
|
|
||||||
return this.prisma.user.delete({
|
return this.prisma.user.delete({
|
||||||
where
|
where
|
||||||
});
|
});
|
||||||
|
@ -1,27 +1,27 @@
|
|||||||
import { Currency, Platform } from '@prisma/client';
|
import { Account, Currency, Platform } from '@prisma/client';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import { IOrder } from '../services/interfaces/interfaces';
|
import { IOrder } from '../services/interfaces/interfaces';
|
||||||
import { OrderType } from './order-type';
|
import { OrderType } from './order-type';
|
||||||
|
|
||||||
export class Order {
|
export class Order {
|
||||||
|
private account: Account;
|
||||||
private currency: Currency;
|
private currency: Currency;
|
||||||
private fee: number;
|
private fee: number;
|
||||||
private date: string;
|
private date: string;
|
||||||
private id: string;
|
private id: string;
|
||||||
private quantity: number;
|
private quantity: number;
|
||||||
private platform: Platform;
|
|
||||||
private symbol: string;
|
private symbol: string;
|
||||||
private total: number;
|
private total: number;
|
||||||
private type: OrderType;
|
private type: OrderType;
|
||||||
private unitPrice: number;
|
private unitPrice: number;
|
||||||
|
|
||||||
public constructor(data: IOrder) {
|
public constructor(data: IOrder) {
|
||||||
|
this.account = data.account;
|
||||||
this.currency = data.currency;
|
this.currency = data.currency;
|
||||||
this.fee = data.fee;
|
this.fee = data.fee;
|
||||||
this.date = data.date;
|
this.date = data.date;
|
||||||
this.id = data.id || uuidv4();
|
this.id = data.id || uuidv4();
|
||||||
this.platform = data.platform;
|
|
||||||
this.quantity = data.quantity;
|
this.quantity = data.quantity;
|
||||||
this.symbol = data.symbol;
|
this.symbol = data.symbol;
|
||||||
this.type = data.type;
|
this.type = data.type;
|
||||||
@ -30,6 +30,10 @@ export class Order {
|
|||||||
this.total = this.quantity * data.unitPrice;
|
this.total = this.quantity * data.unitPrice;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getAccount() {
|
||||||
|
return this.account;
|
||||||
|
}
|
||||||
|
|
||||||
public getCurrency() {
|
public getCurrency() {
|
||||||
return this.currency;
|
return this.currency;
|
||||||
}
|
}
|
||||||
@ -46,10 +50,6 @@ export class Order {
|
|||||||
return this.id;
|
return this.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPlatform() {
|
|
||||||
return this.platform;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getQuantity() {
|
public getQuantity() {
|
||||||
return this.quantity;
|
return this.quantity;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
import { baseCurrency, getUtc, getYesterday } from '@ghostfolio/helper';
|
import {
|
||||||
|
UNKNOWN_KEY,
|
||||||
|
baseCurrency,
|
||||||
|
getUtc,
|
||||||
|
getYesterday
|
||||||
|
} from '@ghostfolio/helper';
|
||||||
import { Test } from '@nestjs/testing';
|
import { Test } from '@nestjs/testing';
|
||||||
import { Currency, Role, Type } from '@prisma/client';
|
import { AccountType, Currency, DataSource, Role, Type } from '@prisma/client';
|
||||||
|
|
||||||
import { ConfigurationService } from '../services/configuration.service';
|
import { ConfigurationService } from '../services/configuration.service';
|
||||||
import { DataProviderService } from '../services/data-provider.service';
|
import { DataProviderService } from '../services/data-provider.service';
|
||||||
@ -70,6 +75,18 @@ describe('Portfolio', () => {
|
|||||||
);
|
);
|
||||||
portfolio.setUser({
|
portfolio.setUser({
|
||||||
accessToken: null,
|
accessToken: null,
|
||||||
|
Account: [
|
||||||
|
{
|
||||||
|
accountType: AccountType.SECURITIES,
|
||||||
|
createdAt: new Date(),
|
||||||
|
id: DEFAULT_ACCOUNT_ID,
|
||||||
|
isDefault: true,
|
||||||
|
name: 'Default Account',
|
||||||
|
platformId: null,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
userId: USER_ID
|
||||||
|
}
|
||||||
|
],
|
||||||
alias: 'Test',
|
alias: 'Test',
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
id: USER_ID,
|
id: USER_ID,
|
||||||
@ -133,10 +150,10 @@ describe('Portfolio', () => {
|
|||||||
accountUserId: USER_ID,
|
accountUserId: USER_ID,
|
||||||
createdAt: null,
|
createdAt: null,
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
fee: 0,
|
fee: 0,
|
||||||
date: new Date(),
|
date: new Date(),
|
||||||
id: '8d999347-dee2-46ee-88e1-26b344e71fcc',
|
id: '8d999347-dee2-46ee-88e1-26b344e71fcc',
|
||||||
platformId: null,
|
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
symbol: 'BTCUSD',
|
symbol: 'BTCUSD',
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
@ -157,20 +174,8 @@ describe('Portfolio', () => {
|
|||||||
const details = await portfolio.getDetails('1d');
|
const details = await portfolio.getDetails('1d');
|
||||||
expect(details).toMatchObject({
|
expect(details).toMatchObject({
|
||||||
BTCUSD: {
|
BTCUSD: {
|
||||||
currency: Currency.USD,
|
accounts: {
|
||||||
exchange: 'Other',
|
[UNKNOWN_KEY]: {
|
||||||
grossPerformance: 0,
|
|
||||||
grossPerformancePercent: 0,
|
|
||||||
investment: exchangeRateDataService.toCurrency(
|
|
||||||
1 * 49631.24,
|
|
||||||
Currency.USD,
|
|
||||||
baseCurrency
|
|
||||||
),
|
|
||||||
// marketPrice: 57973.008,
|
|
||||||
marketState: MarketState.open,
|
|
||||||
name: 'Bitcoin USD',
|
|
||||||
platforms: {
|
|
||||||
Other: {
|
|
||||||
/*current: exchangeRateDataService.toCurrency(
|
/*current: exchangeRateDataService.toCurrency(
|
||||||
1 * 49631.24,
|
1 * 49631.24,
|
||||||
Currency.USD,
|
Currency.USD,
|
||||||
@ -183,10 +188,23 @@ describe('Portfolio', () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// allocationCurrent: 0.9999999559148652,
|
||||||
|
allocationInvestment: 1,
|
||||||
|
currency: Currency.USD,
|
||||||
|
exchange: UNKNOWN_KEY,
|
||||||
|
grossPerformance: 0,
|
||||||
|
grossPerformancePercent: 0,
|
||||||
|
investment: exchangeRateDataService.toCurrency(
|
||||||
|
1 * 49631.24,
|
||||||
|
Currency.USD,
|
||||||
|
baseCurrency
|
||||||
|
),
|
||||||
|
// marketPrice: 57973.008,
|
||||||
|
marketState: MarketState.open,
|
||||||
|
name: 'Bitcoin USD',
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
// shareCurrent: 0.9999999559148652,
|
|
||||||
shareInvestment: 1,
|
|
||||||
symbol: 'BTCUSD',
|
symbol: 'BTCUSD',
|
||||||
|
transactionCount: 1,
|
||||||
type: 'Cryptocurrency'
|
type: 'Cryptocurrency'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -233,10 +251,10 @@ describe('Portfolio', () => {
|
|||||||
accountUserId: USER_ID,
|
accountUserId: USER_ID,
|
||||||
createdAt: null,
|
createdAt: null,
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
fee: 0,
|
fee: 0,
|
||||||
date: new Date(getUtc('2018-01-05')),
|
date: new Date(getUtc('2018-01-05')),
|
||||||
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fb',
|
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fb',
|
||||||
platformId: null,
|
|
||||||
quantity: 0.2,
|
quantity: 0.2,
|
||||||
symbol: 'ETHUSD',
|
symbol: 'ETHUSD',
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
@ -257,19 +275,8 @@ describe('Portfolio', () => {
|
|||||||
const details = await portfolio.getDetails('1d');
|
const details = await portfolio.getDetails('1d');
|
||||||
expect(details).toMatchObject({
|
expect(details).toMatchObject({
|
||||||
ETHUSD: {
|
ETHUSD: {
|
||||||
currency: Currency.USD,
|
accounts: {
|
||||||
exchange: 'Other',
|
[UNKNOWN_KEY]: {
|
||||||
// grossPerformance: 0,
|
|
||||||
// grossPerformancePercent: 0,
|
|
||||||
investment: exchangeRateDataService.toCurrency(
|
|
||||||
0.2 * 991.49,
|
|
||||||
Currency.USD,
|
|
||||||
baseCurrency
|
|
||||||
),
|
|
||||||
// marketPrice: 57973.008,
|
|
||||||
name: 'Ethereum USD',
|
|
||||||
platforms: {
|
|
||||||
Other: {
|
|
||||||
/*current: exchangeRateDataService.toCurrency(
|
/*current: exchangeRateDataService.toCurrency(
|
||||||
0.2 * 991.49,
|
0.2 * 991.49,
|
||||||
Currency.USD,
|
Currency.USD,
|
||||||
@ -282,9 +289,21 @@ describe('Portfolio', () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// allocationCurrent: 1,
|
||||||
|
allocationInvestment: 1,
|
||||||
|
currency: Currency.USD,
|
||||||
|
exchange: UNKNOWN_KEY,
|
||||||
|
// grossPerformance: 0,
|
||||||
|
// grossPerformancePercent: 0,
|
||||||
|
investment: exchangeRateDataService.toCurrency(
|
||||||
|
0.2 * 991.49,
|
||||||
|
Currency.USD,
|
||||||
|
baseCurrency
|
||||||
|
),
|
||||||
|
// marketPrice: 57973.008,
|
||||||
|
name: 'Ethereum USD',
|
||||||
quantity: 0.2,
|
quantity: 0.2,
|
||||||
// shareCurrent: 1,
|
transactionCount: 1,
|
||||||
shareInvestment: 1,
|
|
||||||
symbol: 'ETHUSD',
|
symbol: 'ETHUSD',
|
||||||
type: 'Cryptocurrency'
|
type: 'Cryptocurrency'
|
||||||
}
|
}
|
||||||
@ -327,10 +346,10 @@ describe('Portfolio', () => {
|
|||||||
accountUserId: USER_ID,
|
accountUserId: USER_ID,
|
||||||
createdAt: null,
|
createdAt: null,
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
fee: 0,
|
fee: 0,
|
||||||
date: new Date(getUtc('2018-01-05')),
|
date: new Date(getUtc('2018-01-05')),
|
||||||
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fb',
|
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fb',
|
||||||
platformId: null,
|
|
||||||
quantity: 0.2,
|
quantity: 0.2,
|
||||||
symbol: 'ETHUSD',
|
symbol: 'ETHUSD',
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
@ -343,10 +362,10 @@ describe('Portfolio', () => {
|
|||||||
accountUserId: USER_ID,
|
accountUserId: USER_ID,
|
||||||
createdAt: null,
|
createdAt: null,
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
fee: 0,
|
fee: 0,
|
||||||
date: new Date(getUtc('2018-01-28')),
|
date: new Date(getUtc('2018-01-28')),
|
||||||
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fc',
|
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fc',
|
||||||
platformId: null,
|
|
||||||
quantity: 0.3,
|
quantity: 0.3,
|
||||||
symbol: 'ETHUSD',
|
symbol: 'ETHUSD',
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
@ -403,10 +422,10 @@ describe('Portfolio', () => {
|
|||||||
accountUserId: USER_ID,
|
accountUserId: USER_ID,
|
||||||
createdAt: null,
|
createdAt: null,
|
||||||
currency: Currency.EUR,
|
currency: Currency.EUR,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
date: new Date(getUtc('2017-08-16')),
|
date: new Date(getUtc('2017-08-16')),
|
||||||
fee: 2.99,
|
fee: 2.99,
|
||||||
id: 'd96795b2-6ae6-420e-aa21-fabe5e45d475',
|
id: 'd96795b2-6ae6-420e-aa21-fabe5e45d475',
|
||||||
platformId: null,
|
|
||||||
quantity: 0.05614682,
|
quantity: 0.05614682,
|
||||||
symbol: 'BTCUSD',
|
symbol: 'BTCUSD',
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
@ -419,10 +438,10 @@ describe('Portfolio', () => {
|
|||||||
accountUserId: USER_ID,
|
accountUserId: USER_ID,
|
||||||
createdAt: null,
|
createdAt: null,
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
fee: 2.99,
|
fee: 2.99,
|
||||||
date: new Date(getUtc('2018-01-05')),
|
date: new Date(getUtc('2018-01-05')),
|
||||||
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fb',
|
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fb',
|
||||||
platformId: null,
|
|
||||||
quantity: 0.2,
|
quantity: 0.2,
|
||||||
symbol: 'ETHUSD',
|
symbol: 'ETHUSD',
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
@ -492,10 +511,10 @@ describe('Portfolio', () => {
|
|||||||
accountUserId: USER_ID,
|
accountUserId: USER_ID,
|
||||||
createdAt: null,
|
createdAt: null,
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
fee: 1.0,
|
fee: 1.0,
|
||||||
date: new Date(getUtc('2018-01-05')),
|
date: new Date(getUtc('2018-01-05')),
|
||||||
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fb',
|
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fb',
|
||||||
platformId: null,
|
|
||||||
quantity: 0.2,
|
quantity: 0.2,
|
||||||
symbol: 'ETHUSD',
|
symbol: 'ETHUSD',
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
@ -508,10 +527,10 @@ describe('Portfolio', () => {
|
|||||||
accountUserId: USER_ID,
|
accountUserId: USER_ID,
|
||||||
createdAt: null,
|
createdAt: null,
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
fee: 1.0,
|
fee: 1.0,
|
||||||
date: new Date(getUtc('2018-01-28')),
|
date: new Date(getUtc('2018-01-28')),
|
||||||
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fc',
|
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fc',
|
||||||
platformId: null,
|
|
||||||
quantity: 0.1,
|
quantity: 0.1,
|
||||||
symbol: 'ETHUSD',
|
symbol: 'ETHUSD',
|
||||||
type: Type.SELL,
|
type: Type.SELL,
|
||||||
@ -524,10 +543,10 @@ describe('Portfolio', () => {
|
|||||||
accountUserId: USER_ID,
|
accountUserId: USER_ID,
|
||||||
createdAt: null,
|
createdAt: null,
|
||||||
currency: Currency.USD,
|
currency: Currency.USD,
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
fee: 1.0,
|
fee: 1.0,
|
||||||
date: new Date(getUtc('2018-01-31')),
|
date: new Date(getUtc('2018-01-31')),
|
||||||
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fc',
|
id: '4a5a5c6e-659d-45cc-9fd4-fd6c873b50fc',
|
||||||
platformId: null,
|
|
||||||
quantity: 0.2,
|
quantity: 0.2,
|
||||||
symbol: 'ETHUSD',
|
symbol: 'ETHUSD',
|
||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
|
@ -2,7 +2,12 @@ import {
|
|||||||
PortfolioItem,
|
PortfolioItem,
|
||||||
Position
|
Position
|
||||||
} from '@ghostfolio/api/app/portfolio/interfaces/portfolio-item.interface';
|
} from '@ghostfolio/api/app/portfolio/interfaces/portfolio-item.interface';
|
||||||
import { getToday, getYesterday, resetHours } from '@ghostfolio/helper';
|
import {
|
||||||
|
UNKNOWN_KEY,
|
||||||
|
getToday,
|
||||||
|
getYesterday,
|
||||||
|
resetHours
|
||||||
|
} from '@ghostfolio/helper';
|
||||||
import {
|
import {
|
||||||
add,
|
add,
|
||||||
format,
|
format,
|
||||||
@ -23,7 +28,7 @@ import { cloneDeep, isEmpty } from 'lodash';
|
|||||||
import * as roundTo from 'round-to';
|
import * as roundTo from 'round-to';
|
||||||
|
|
||||||
import { UserWithSettings } from '../app/interfaces/user-with-settings';
|
import { UserWithSettings } from '../app/interfaces/user-with-settings';
|
||||||
import { OrderWithPlatform } from '../app/order/interfaces/order-with-platform.type';
|
import { OrderWithAccount } from '../app/order/interfaces/order-with-account.type';
|
||||||
import { DateRange } from '../app/portfolio/interfaces/date-range.type';
|
import { DateRange } from '../app/portfolio/interfaces/date-range.type';
|
||||||
import { PortfolioPerformance } from '../app/portfolio/interfaces/portfolio-performance.interface';
|
import { PortfolioPerformance } from '../app/portfolio/interfaces/portfolio-performance.interface';
|
||||||
import { PortfolioPosition } from '../app/portfolio/interfaces/portfolio-position.interface';
|
import { PortfolioPosition } from '../app/portfolio/interfaces/portfolio-position.interface';
|
||||||
@ -34,14 +39,15 @@ import { IOrder } 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';
|
||||||
|
import { OrderType } from './order-type';
|
||||||
|
import { AccountClusterRiskCurrentInvestment } from './rules/account-cluster-risk/current-investment';
|
||||||
|
import { AccountClusterRiskInitialInvestment } from './rules/account-cluster-risk/initial-investment';
|
||||||
|
import { AccountClusterRiskSingleAccount } from './rules/account-cluster-risk/single-account';
|
||||||
import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from './rules/currency-cluster-risk/base-currency-current-investment';
|
import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from './rules/currency-cluster-risk/base-currency-current-investment';
|
||||||
import { CurrencyClusterRiskBaseCurrencyInitialInvestment } from './rules/currency-cluster-risk/base-currency-initial-investment';
|
import { CurrencyClusterRiskBaseCurrencyInitialInvestment } from './rules/currency-cluster-risk/base-currency-initial-investment';
|
||||||
import { CurrencyClusterRiskCurrentInvestment } from './rules/currency-cluster-risk/current-investment';
|
import { CurrencyClusterRiskCurrentInvestment } from './rules/currency-cluster-risk/current-investment';
|
||||||
import { CurrencyClusterRiskInitialInvestment } from './rules/currency-cluster-risk/initial-investment';
|
import { CurrencyClusterRiskInitialInvestment } from './rules/currency-cluster-risk/initial-investment';
|
||||||
import { FeeRatioInitialInvestment } from './rules/fees/fee-ratio-initial-investment';
|
import { FeeRatioInitialInvestment } from './rules/fees/fee-ratio-initial-investment';
|
||||||
import { PlatformClusterRiskCurrentInvestment } from './rules/platform-cluster-risk/current-investment';
|
|
||||||
import { PlatformClusterRiskInitialInvestment } from './rules/platform-cluster-risk/initial-investment';
|
|
||||||
import { PlatformClusterRiskSinglePlatform } from './rules/platform-cluster-risk/single-platform';
|
|
||||||
|
|
||||||
export class Portfolio implements PortfolioInterface {
|
export class Portfolio implements PortfolioInterface {
|
||||||
private orders: Order[] = [];
|
private orders: Order[] = [];
|
||||||
@ -57,7 +63,7 @@ export class Portfolio implements PortfolioInterface {
|
|||||||
public async addCurrentPortfolioItems() {
|
public async addCurrentPortfolioItems() {
|
||||||
const currentData = await this.dataProviderService.get(this.getSymbols());
|
const currentData = await this.dataProviderService.get(this.getSymbols());
|
||||||
|
|
||||||
let currentDate = new Date();
|
const currentDate = new Date();
|
||||||
|
|
||||||
const year = getYear(currentDate);
|
const year = getYear(currentDate);
|
||||||
const month = getMonth(currentDate);
|
const month = getMonth(currentDate);
|
||||||
@ -82,7 +88,9 @@ export class Portfolio implements PortfolioInterface {
|
|||||||
marketPrice:
|
marketPrice:
|
||||||
currentData[symbol]?.marketPrice ??
|
currentData[symbol]?.marketPrice ??
|
||||||
portfolioItemsYesterday.positions[symbol]?.marketPrice,
|
portfolioItemsYesterday.positions[symbol]?.marketPrice,
|
||||||
quantity: portfolioItemsYesterday?.positions[symbol]?.quantity
|
quantity: portfolioItemsYesterday?.positions[symbol]?.quantity,
|
||||||
|
transactionCount:
|
||||||
|
portfolioItemsYesterday?.positions[symbol]?.transactionCount
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -117,11 +125,11 @@ export class Portfolio implements PortfolioInterface {
|
|||||||
}): Portfolio {
|
}): Portfolio {
|
||||||
orders.forEach(
|
orders.forEach(
|
||||||
({
|
({
|
||||||
|
account,
|
||||||
currency,
|
currency,
|
||||||
fee,
|
fee,
|
||||||
date,
|
date,
|
||||||
id,
|
id,
|
||||||
platform,
|
|
||||||
quantity,
|
quantity,
|
||||||
symbol,
|
symbol,
|
||||||
type,
|
type,
|
||||||
@ -129,11 +137,11 @@ export class Portfolio implements PortfolioInterface {
|
|||||||
}) => {
|
}) => {
|
||||||
this.orders.push(
|
this.orders.push(
|
||||||
new Order({
|
new Order({
|
||||||
|
account,
|
||||||
currency,
|
currency,
|
||||||
fee,
|
fee,
|
||||||
date,
|
date,
|
||||||
id,
|
id,
|
||||||
platform,
|
|
||||||
quantity,
|
quantity,
|
||||||
symbol,
|
symbol,
|
||||||
type,
|
type,
|
||||||
@ -200,7 +208,7 @@ export class Portfolio implements PortfolioInterface {
|
|||||||
const data = await this.dataProviderService.get(symbols);
|
const data = await this.dataProviderService.get(symbols);
|
||||||
|
|
||||||
symbols.forEach((symbol) => {
|
symbols.forEach((symbol) => {
|
||||||
const platforms: PortfolioPosition['platforms'] = {};
|
const accounts: PortfolioPosition['accounts'] = {};
|
||||||
const [portfolioItem] = portfolioItems;
|
const [portfolioItem] = portfolioItems;
|
||||||
|
|
||||||
const ordersBySymbol = this.getOrders().filter((order) => {
|
const ordersBySymbol = this.getOrders().filter((order) => {
|
||||||
@ -225,15 +233,17 @@ export class Portfolio implements PortfolioInterface {
|
|||||||
originalValueOfSymbol *= -1;
|
originalValueOfSymbol *= -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (platforms[orderOfSymbol.getPlatform()?.name || 'Other']?.current) {
|
if (
|
||||||
platforms[
|
accounts[orderOfSymbol.getAccount()?.name || UNKNOWN_KEY]?.current
|
||||||
orderOfSymbol.getPlatform()?.name || 'Other'
|
) {
|
||||||
|
accounts[
|
||||||
|
orderOfSymbol.getAccount()?.name || UNKNOWN_KEY
|
||||||
].current += currentValueOfSymbol;
|
].current += currentValueOfSymbol;
|
||||||
platforms[
|
accounts[
|
||||||
orderOfSymbol.getPlatform()?.name || 'Other'
|
orderOfSymbol.getAccount()?.name || UNKNOWN_KEY
|
||||||
].original += originalValueOfSymbol;
|
].original += originalValueOfSymbol;
|
||||||
} else {
|
} else {
|
||||||
platforms[orderOfSymbol.getPlatform()?.name || 'Other'] = {
|
accounts[orderOfSymbol.getAccount()?.name || UNKNOWN_KEY] = {
|
||||||
current: currentValueOfSymbol,
|
current: currentValueOfSymbol,
|
||||||
original: originalValueOfSymbol
|
original: originalValueOfSymbol
|
||||||
};
|
};
|
||||||
@ -274,8 +284,16 @@ export class Portfolio implements PortfolioInterface {
|
|||||||
|
|
||||||
details[symbol] = {
|
details[symbol] = {
|
||||||
...data[symbol],
|
...data[symbol],
|
||||||
platforms,
|
accounts,
|
||||||
symbol,
|
symbol,
|
||||||
|
allocationCurrent:
|
||||||
|
this.exchangeRateDataService.toCurrency(
|
||||||
|
portfolioItem.positions[symbol].quantity * now,
|
||||||
|
data[symbol]?.currency,
|
||||||
|
this.user.Settings.currency
|
||||||
|
) / value,
|
||||||
|
allocationInvestment:
|
||||||
|
portfolioItem.positions[symbol].investment / investment,
|
||||||
grossPerformance: roundTo(
|
grossPerformance: roundTo(
|
||||||
portfolioItemsNow.positions[symbol].quantity * (now - before),
|
portfolioItemsNow.positions[symbol].quantity * (now - before),
|
||||||
2
|
2
|
||||||
@ -283,13 +301,7 @@ export class Portfolio implements PortfolioInterface {
|
|||||||
grossPerformancePercent: roundTo((now - before) / before, 4),
|
grossPerformancePercent: roundTo((now - before) / before, 4),
|
||||||
investment: portfolioItem.positions[symbol].investment,
|
investment: portfolioItem.positions[symbol].investment,
|
||||||
quantity: portfolioItem.positions[symbol].quantity,
|
quantity: portfolioItem.positions[symbol].quantity,
|
||||||
shareCurrent:
|
transactionCount: portfolioItem.positions[symbol].transactionCount
|
||||||
this.exchangeRateDataService.toCurrency(
|
|
||||||
portfolioItem.positions[symbol].quantity * now,
|
|
||||||
data[symbol]?.currency,
|
|
||||||
this.user.Settings.currency
|
|
||||||
) / value,
|
|
||||||
shareInvestment: portfolioItem.positions[symbol].investment / investment
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -392,6 +404,19 @@ export class Portfolio implements PortfolioInterface {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
rules: {
|
rules: {
|
||||||
|
accountClusterRisk: await this.rulesService.evaluate(
|
||||||
|
this,
|
||||||
|
[
|
||||||
|
new AccountClusterRiskCurrentInvestment(
|
||||||
|
this.exchangeRateDataService
|
||||||
|
),
|
||||||
|
new AccountClusterRiskInitialInvestment(
|
||||||
|
this.exchangeRateDataService
|
||||||
|
),
|
||||||
|
new AccountClusterRiskSingleAccount(this.exchangeRateDataService)
|
||||||
|
],
|
||||||
|
{ baseCurrency: this.user.Settings.currency }
|
||||||
|
),
|
||||||
currencyClusterRisk: await this.rulesService.evaluate(
|
currencyClusterRisk: await this.rulesService.evaluate(
|
||||||
this,
|
this,
|
||||||
[
|
[
|
||||||
@ -410,19 +435,6 @@ export class Portfolio implements PortfolioInterface {
|
|||||||
],
|
],
|
||||||
{ baseCurrency: this.user.Settings.currency }
|
{ baseCurrency: this.user.Settings.currency }
|
||||||
),
|
),
|
||||||
platformClusterRisk: await this.rulesService.evaluate(
|
|
||||||
this,
|
|
||||||
[
|
|
||||||
new PlatformClusterRiskSinglePlatform(this.exchangeRateDataService),
|
|
||||||
new PlatformClusterRiskInitialInvestment(
|
|
||||||
this.exchangeRateDataService
|
|
||||||
),
|
|
||||||
new PlatformClusterRiskCurrentInvestment(
|
|
||||||
this.exchangeRateDataService
|
|
||||||
)
|
|
||||||
],
|
|
||||||
{ baseCurrency: this.user.Settings.currency }
|
|
||||||
),
|
|
||||||
fees: await this.rulesService.evaluate(
|
fees: await this.rulesService.evaluate(
|
||||||
this,
|
this,
|
||||||
[new FeeRatioInitialInvestment(this.exchangeRateDataService)],
|
[new FeeRatioInitialInvestment(this.exchangeRateDataService)],
|
||||||
@ -518,20 +530,20 @@ export class Portfolio implements PortfolioInterface {
|
|||||||
return isFinite(value) ? value : null;
|
return isFinite(value) ? value : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setOrders(aOrders: OrderWithPlatform[]) {
|
public async setOrders(aOrders: OrderWithAccount[]) {
|
||||||
this.orders = [];
|
this.orders = [];
|
||||||
|
|
||||||
// Map data
|
// Map data
|
||||||
aOrders.forEach((order) => {
|
aOrders.forEach((order) => {
|
||||||
this.orders.push(
|
this.orders.push(
|
||||||
new Order({
|
new Order({
|
||||||
currency: <any>order.currency,
|
account: order.Account,
|
||||||
|
currency: order.currency,
|
||||||
date: order.date.toISOString(),
|
date: order.date.toISOString(),
|
||||||
fee: order.fee,
|
fee: order.fee,
|
||||||
platform: order.Platform,
|
|
||||||
quantity: order.quantity,
|
quantity: order.quantity,
|
||||||
symbol: order.symbol,
|
symbol: order.symbol,
|
||||||
type: <any>order.type,
|
type: <OrderType>order.type,
|
||||||
unitPrice: order.unitPrice
|
unitPrice: order.unitPrice
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -582,7 +594,8 @@ export class Portfolio implements PortfolioInterface {
|
|||||||
marketPrice:
|
marketPrice:
|
||||||
historicalData[symbol]?.[format(currentDate, 'yyyy-MM-dd')]
|
historicalData[symbol]?.[format(currentDate, 'yyyy-MM-dd')]
|
||||||
?.marketPrice || 0,
|
?.marketPrice || 0,
|
||||||
quantity: 0
|
quantity: 0,
|
||||||
|
transactionCount: 0
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -623,7 +636,8 @@ export class Portfolio implements PortfolioInterface {
|
|||||||
marketPrice:
|
marketPrice:
|
||||||
historicalData[symbol]?.[format(yesterday, 'yyyy-MM-dd')]
|
historicalData[symbol]?.[format(yesterday, 'yyyy-MM-dd')]
|
||||||
?.marketPrice || 0,
|
?.marketPrice || 0,
|
||||||
quantity: 0
|
quantity: 0,
|
||||||
|
transactionCount: 0
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -730,6 +744,10 @@ export class Portfolio implements PortfolioInterface {
|
|||||||
order.getSymbol()
|
order.getSymbol()
|
||||||
].currency = order.getCurrency();
|
].currency = order.getCurrency();
|
||||||
|
|
||||||
|
this.portfolioItems[i].positions[
|
||||||
|
order.getSymbol()
|
||||||
|
].transactionCount += 1;
|
||||||
|
|
||||||
if (order.getType() === 'BUY') {
|
if (order.getType() === 'BUY') {
|
||||||
if (
|
if (
|
||||||
!this.portfolioItems[i].positions[order.getSymbol()].firstBuyDate
|
!this.portfolioItems[i].positions[order.getSymbol()].firstBuyDate
|
||||||
|
@ -3,7 +3,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
|
|||||||
|
|
||||||
import { Rule } from '../../rule';
|
import { Rule } from '../../rule';
|
||||||
|
|
||||||
export class PlatformClusterRiskCurrentInvestment extends Rule {
|
export class AccountClusterRiskCurrentInvestment extends Rule {
|
||||||
public constructor(public exchangeRateDataService: ExchangeRateDataService) {
|
public constructor(public exchangeRateDataService: ExchangeRateDataService) {
|
||||||
super(exchangeRateDataService, {
|
super(exchangeRateDataService, {
|
||||||
name: 'Current Investment'
|
name: 'Current Investment'
|
||||||
@ -18,24 +18,22 @@ export class PlatformClusterRiskCurrentInvestment extends Rule {
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const ruleSettings =
|
const ruleSettings =
|
||||||
aRuleSettingsMap[PlatformClusterRiskCurrentInvestment.name];
|
aRuleSettingsMap[AccountClusterRiskCurrentInvestment.name];
|
||||||
|
|
||||||
const platforms: {
|
const accounts: {
|
||||||
[symbol: string]: Pick<PortfolioPosition, 'name'> & {
|
[symbol: string]: Pick<PortfolioPosition, 'name'> & {
|
||||||
investment: number;
|
investment: number;
|
||||||
};
|
};
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
Object.values(aPositions).forEach((position) => {
|
Object.values(aPositions).forEach((position) => {
|
||||||
for (const [platform, { current }] of Object.entries(
|
for (const [account, { current }] of Object.entries(position.accounts)) {
|
||||||
position.platforms
|
if (accounts[account]?.investment) {
|
||||||
)) {
|
accounts[account].investment += current;
|
||||||
if (platforms[platform]?.investment) {
|
|
||||||
platforms[platform].investment += current;
|
|
||||||
} else {
|
} else {
|
||||||
platforms[platform] = {
|
accounts[account] = {
|
||||||
investment: current,
|
investment: current,
|
||||||
name: platform
|
name: account
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -44,17 +42,17 @@ export class PlatformClusterRiskCurrentInvestment extends Rule {
|
|||||||
let maxItem;
|
let maxItem;
|
||||||
let totalInvestment = 0;
|
let totalInvestment = 0;
|
||||||
|
|
||||||
Object.values(platforms).forEach((platform) => {
|
Object.values(accounts).forEach((account) => {
|
||||||
if (!maxItem) {
|
if (!maxItem) {
|
||||||
maxItem = platform;
|
maxItem = account;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate total investment
|
// Calculate total investment
|
||||||
totalInvestment += platform.investment;
|
totalInvestment += account.investment;
|
||||||
|
|
||||||
// Find maximum
|
// Find maximum
|
||||||
if (platform.investment > maxItem?.investment) {
|
if (account.investment > maxItem?.investment) {
|
||||||
maxItem = platform;
|
maxItem = account;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -3,7 +3,7 @@ import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-dat
|
|||||||
|
|
||||||
import { Rule } from '../../rule';
|
import { Rule } from '../../rule';
|
||||||
|
|
||||||
export class PlatformClusterRiskInitialInvestment extends Rule {
|
export class AccountClusterRiskInitialInvestment extends Rule {
|
||||||
public constructor(public exchangeRateDataService: ExchangeRateDataService) {
|
public constructor(public exchangeRateDataService: ExchangeRateDataService) {
|
||||||
super(exchangeRateDataService, {
|
super(exchangeRateDataService, {
|
||||||
name: 'Initial Investment'
|
name: 'Initial Investment'
|
||||||
@ -18,7 +18,7 @@ export class PlatformClusterRiskInitialInvestment extends Rule {
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const ruleSettings =
|
const ruleSettings =
|
||||||
aRuleSettingsMap[PlatformClusterRiskInitialInvestment.name];
|
aRuleSettingsMap[AccountClusterRiskInitialInvestment.name];
|
||||||
|
|
||||||
const platforms: {
|
const platforms: {
|
||||||
[symbol: string]: Pick<PortfolioPosition, 'name'> & {
|
[symbol: string]: Pick<PortfolioPosition, 'name'> & {
|
||||||
@ -27,15 +27,13 @@ export class PlatformClusterRiskInitialInvestment extends Rule {
|
|||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
Object.values(aPositions).forEach((position) => {
|
Object.values(aPositions).forEach((position) => {
|
||||||
for (const [platform, { original }] of Object.entries(
|
for (const [account, { original }] of Object.entries(position.accounts)) {
|
||||||
position.platforms
|
if (platforms[account]?.investment) {
|
||||||
)) {
|
platforms[account].investment += original;
|
||||||
if (platforms[platform]?.investment) {
|
|
||||||
platforms[platform].investment += original;
|
|
||||||
} else {
|
} else {
|
||||||
platforms[platform] = {
|
platforms[account] = {
|
||||||
investment: original,
|
investment: original,
|
||||||
name: platform
|
name: account
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,33 +3,33 @@ import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-dat
|
|||||||
|
|
||||||
import { Rule } from '../../rule';
|
import { Rule } from '../../rule';
|
||||||
|
|
||||||
export class PlatformClusterRiskSinglePlatform extends Rule {
|
export class AccountClusterRiskSingleAccount extends Rule {
|
||||||
public constructor(public exchangeRateDataService: ExchangeRateDataService) {
|
public constructor(public exchangeRateDataService: ExchangeRateDataService) {
|
||||||
super(exchangeRateDataService, {
|
super(exchangeRateDataService, {
|
||||||
name: 'Single Platform'
|
name: 'Single Account'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public evaluate(positions: { [symbol: string]: PortfolioPosition }) {
|
public evaluate(positions: { [symbol: string]: PortfolioPosition }) {
|
||||||
const platforms: string[] = [];
|
const accounts: string[] = [];
|
||||||
|
|
||||||
Object.values(positions).forEach((position) => {
|
Object.values(positions).forEach((position) => {
|
||||||
for (const [platform] of Object.entries(position.platforms)) {
|
for (const [account] of Object.entries(position.accounts)) {
|
||||||
if (!platforms.includes(platform)) {
|
if (!accounts.includes(account)) {
|
||||||
platforms.push(platform);
|
accounts.push(account);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (platforms.length === 1) {
|
if (accounts.length === 1) {
|
||||||
return {
|
return {
|
||||||
evaluation: `All your investment is managed by a single platform`,
|
evaluation: `All your investment is managed by a single account`,
|
||||||
value: false
|
value: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
evaluation: `Your investment is managed by ${platforms.length} platforms`,
|
evaluation: `Your investment is managed by ${accounts.length} accounts`,
|
||||||
value: true
|
value: true
|
||||||
};
|
};
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import { DataSource } from '.prisma/client';
|
|
||||||
import { getYesterday } from '@ghostfolio/helper';
|
import { getYesterday } from '@ghostfolio/helper';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DataSource } from '@prisma/client';
|
||||||
import * as bent from 'bent';
|
import * as bent from 'bent';
|
||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Currency } from '.prisma/client';
|
import { Currency } from '@prisma/client';
|
||||||
|
|
||||||
export interface ScraperConfig {
|
export interface ScraperConfig {
|
||||||
currency: Currency;
|
currency: Currency;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { DataSource } from '.prisma/client';
|
|
||||||
import { getToday, getYesterday } from '@ghostfolio/helper';
|
import { getToday, getYesterday } from '@ghostfolio/helper';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DataSource } from '@prisma/client';
|
||||||
import * as bent from 'bent';
|
import * as bent from 'bent';
|
||||||
import { format, subMonths, subWeeks, subYears } from 'date-fns';
|
import { format, subMonths, subWeeks, subYears } from 'date-fns';
|
||||||
|
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
import { DataSource } from '.prisma/client';
|
import {
|
||||||
import { isCrypto, isCurrency, parseCurrency } from '@ghostfolio/helper';
|
UNKNOWN_KEY,
|
||||||
|
isCrypto,
|
||||||
|
isCurrency,
|
||||||
|
parseCurrency
|
||||||
|
} from '@ghostfolio/helper';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DataSource } from '@prisma/client';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import * as yahooFinance from 'yahoo-finance';
|
import * as yahooFinance from 'yahoo-finance';
|
||||||
|
|
||||||
@ -170,7 +175,7 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
|
|
||||||
private parseExchange(aString: string): string {
|
private parseExchange(aString: string): string {
|
||||||
if (aString?.toLowerCase() === 'ccc') {
|
if (aString?.toLowerCase() === 'ccc') {
|
||||||
return 'Other';
|
return UNKNOWN_KEY;
|
||||||
}
|
}
|
||||||
|
|
||||||
return aString;
|
return aString;
|
||||||
@ -200,7 +205,7 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
return Industry.Software;
|
return Industry.Software;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Industry.Other;
|
return Industry.Unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseSector(aString: string): Sector {
|
private parseSector(aString: string): Sector {
|
||||||
@ -222,7 +227,7 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
return Sector.Technology;
|
return Sector.Technology;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Sector.Other;
|
return Sector.Unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseType(aString: string): Type {
|
private parseType(aString: string): Type {
|
||||||
@ -234,7 +239,7 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
return Type.Stock;
|
return Type.Stock;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Type.Other;
|
return Type.Unknown;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Currency, DataSource, Platform } from '@prisma/client';
|
import { UNKNOWN_KEY } from '@ghostfolio/helper';
|
||||||
|
import { Account, Currency, DataSource } from '@prisma/client';
|
||||||
|
|
||||||
import { OrderType } from '../../models/order-type';
|
import { OrderType } from '../../models/order-type';
|
||||||
|
|
||||||
@ -7,9 +8,9 @@ export const Industry = {
|
|||||||
Biotechnology: 'Biotechnology',
|
Biotechnology: 'Biotechnology',
|
||||||
Food: 'Food',
|
Food: 'Food',
|
||||||
Internet: 'Internet',
|
Internet: 'Internet',
|
||||||
Other: 'Other',
|
|
||||||
Pharmaceutical: 'Pharmaceutical',
|
Pharmaceutical: 'Pharmaceutical',
|
||||||
Software: 'Software'
|
Software: 'Software',
|
||||||
|
Unknown: UNKNOWN_KEY
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MarketState = {
|
export const MarketState = {
|
||||||
@ -21,23 +22,23 @@ export const MarketState = {
|
|||||||
export const Sector = {
|
export const Sector = {
|
||||||
Consumer: 'Consumer',
|
Consumer: 'Consumer',
|
||||||
Healthcare: 'Healthcare',
|
Healthcare: 'Healthcare',
|
||||||
Other: 'Other',
|
Technology: 'Technology',
|
||||||
Technology: 'Technology'
|
Unknown: UNKNOWN_KEY
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Type = {
|
export const Type = {
|
||||||
Cryptocurrency: 'Cryptocurrency',
|
Cryptocurrency: 'Cryptocurrency',
|
||||||
ETF: 'ETF',
|
ETF: 'ETF',
|
||||||
Other: 'Other',
|
Stock: 'Stock',
|
||||||
Stock: 'Stock'
|
Unknown: UNKNOWN_KEY
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IOrder {
|
export interface IOrder {
|
||||||
|
account: Account;
|
||||||
currency: Currency;
|
currency: Currency;
|
||||||
date: string;
|
date: string;
|
||||||
fee: number;
|
fee: number;
|
||||||
id?: string;
|
id?: string;
|
||||||
platform: Platform;
|
|
||||||
quantity: number;
|
quantity: number;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
type: OrderType;
|
type: OrderType;
|
||||||
|
@ -2,14 +2,14 @@ import { Injectable } from '@nestjs/common';
|
|||||||
|
|
||||||
import { Portfolio } from '../models/portfolio';
|
import { Portfolio } from '../models/portfolio';
|
||||||
import { Rule } from '../models/rule';
|
import { Rule } from '../models/rule';
|
||||||
|
import { AccountClusterRiskCurrentInvestment } from '../models/rules/account-cluster-risk/current-investment';
|
||||||
|
import { AccountClusterRiskInitialInvestment } from '../models/rules/account-cluster-risk/initial-investment';
|
||||||
|
import { AccountClusterRiskSingleAccount } from '../models/rules/account-cluster-risk/single-account';
|
||||||
import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from '../models/rules/currency-cluster-risk/base-currency-current-investment';
|
import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from '../models/rules/currency-cluster-risk/base-currency-current-investment';
|
||||||
import { CurrencyClusterRiskBaseCurrencyInitialInvestment } from '../models/rules/currency-cluster-risk/base-currency-initial-investment';
|
import { CurrencyClusterRiskBaseCurrencyInitialInvestment } from '../models/rules/currency-cluster-risk/base-currency-initial-investment';
|
||||||
import { CurrencyClusterRiskCurrentInvestment } from '../models/rules/currency-cluster-risk/current-investment';
|
import { CurrencyClusterRiskCurrentInvestment } from '../models/rules/currency-cluster-risk/current-investment';
|
||||||
import { CurrencyClusterRiskInitialInvestment } from '../models/rules/currency-cluster-risk/initial-investment';
|
import { CurrencyClusterRiskInitialInvestment } from '../models/rules/currency-cluster-risk/initial-investment';
|
||||||
import { FeeRatioInitialInvestment } from '../models/rules/fees/fee-ratio-initial-investment';
|
import { FeeRatioInitialInvestment } from '../models/rules/fees/fee-ratio-initial-investment';
|
||||||
import { PlatformClusterRiskCurrentInvestment } from '../models/rules/platform-cluster-risk/current-investment';
|
|
||||||
import { PlatformClusterRiskInitialInvestment } from '../models/rules/platform-cluster-risk/initial-investment';
|
|
||||||
import { PlatformClusterRiskSinglePlatform } from '../models/rules/platform-cluster-risk/single-platform';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RulesService {
|
export class RulesService {
|
||||||
@ -39,6 +39,17 @@ export class RulesService {
|
|||||||
|
|
||||||
private getDefaultRuleSettings(aUserSettings: { baseCurrency: string }) {
|
private getDefaultRuleSettings(aUserSettings: { baseCurrency: string }) {
|
||||||
return {
|
return {
|
||||||
|
[AccountClusterRiskCurrentInvestment.name]: {
|
||||||
|
baseCurrency: aUserSettings.baseCurrency,
|
||||||
|
isActive: true,
|
||||||
|
threshold: 0.5
|
||||||
|
},
|
||||||
|
[AccountClusterRiskInitialInvestment.name]: {
|
||||||
|
baseCurrency: aUserSettings.baseCurrency,
|
||||||
|
isActive: true,
|
||||||
|
threshold: 0.5
|
||||||
|
},
|
||||||
|
[AccountClusterRiskSingleAccount.name]: { isActive: true },
|
||||||
[CurrencyClusterRiskBaseCurrencyInitialInvestment.name]: {
|
[CurrencyClusterRiskBaseCurrencyInitialInvestment.name]: {
|
||||||
baseCurrency: aUserSettings.baseCurrency,
|
baseCurrency: aUserSettings.baseCurrency,
|
||||||
isActive: true
|
isActive: true
|
||||||
@ -61,18 +72,7 @@ export class RulesService {
|
|||||||
baseCurrency: aUserSettings.baseCurrency,
|
baseCurrency: aUserSettings.baseCurrency,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
threshold: 0.01
|
threshold: 0.01
|
||||||
},
|
}
|
||||||
[PlatformClusterRiskCurrentInvestment.name]: {
|
|
||||||
baseCurrency: aUserSettings.baseCurrency,
|
|
||||||
isActive: true,
|
|
||||||
threshold: 0.5
|
|
||||||
},
|
|
||||||
[PlatformClusterRiskInitialInvestment.name]: {
|
|
||||||
baseCurrency: aUserSettings.baseCurrency,
|
|
||||||
isActive: true,
|
|
||||||
threshold: 0.5
|
|
||||||
},
|
|
||||||
[PlatformClusterRiskSinglePlatform.name]: { isActive: true }
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,11 +9,6 @@ const routes: Routes = [
|
|||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./pages/about/about-page.module').then((m) => m.AboutPageModule)
|
import('./pages/about/about-page.module').then((m) => m.AboutPageModule)
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'admin',
|
|
||||||
loadChildren: () =>
|
|
||||||
import('./pages/admin/admin-page.module').then((m) => m.AdminPageModule)
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'account',
|
path: 'account',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
@ -22,9 +17,16 @@ const routes: Routes = [
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'auth',
|
path: 'accounts',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./pages/auth/auth-page.module').then((m) => m.AuthPageModule)
|
import('./pages/accounts/accounts-page.module').then(
|
||||||
|
(m) => m.AccountsPageModule
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'admin',
|
||||||
|
loadChildren: () =>
|
||||||
|
import('./pages/admin/admin-page.module').then((m) => m.AdminPageModule)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'analysis',
|
path: 'analysis',
|
||||||
@ -33,11 +35,23 @@ const routes: Routes = [
|
|||||||
(m) => m.AnalysisPageModule
|
(m) => m.AnalysisPageModule
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'auth',
|
||||||
|
loadChildren: () =>
|
||||||
|
import('./pages/auth/auth-page.module').then((m) => m.AuthPageModule)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'home',
|
path: 'home',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./pages/home/home-page.module').then((m) => m.HomePageModule)
|
import('./pages/home/home-page.module').then((m) => m.HomePageModule)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'pricing',
|
||||||
|
loadChildren: () =>
|
||||||
|
import('./pages/pricing/pricing-page.module').then(
|
||||||
|
(m) => m.PricingPageModule
|
||||||
|
)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'report',
|
path: 'report',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
|
@ -5,7 +5,7 @@ import {
|
|||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit
|
OnInit
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { NavigationEnd, Router } from '@angular/router';
|
import { NavigationEnd, PRIMARY_OUTLET, Router } from '@angular/router';
|
||||||
import { InfoItem } from '@ghostfolio/api/app/info/interfaces/info-item.interface';
|
import { InfoItem } from '@ghostfolio/api/app/info/interfaces/info-item.interface';
|
||||||
import { User } from '@ghostfolio/api/app/user/interfaces/user.interface';
|
import { User } from '@ghostfolio/api/app/user/interfaces/user.interface';
|
||||||
import {
|
import {
|
||||||
@ -57,9 +57,11 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
this.router.events
|
this.router.events
|
||||||
.pipe(filter((event) => event instanceof NavigationEnd))
|
.pipe(filter((event) => event instanceof NavigationEnd))
|
||||||
.subscribe((test) => {
|
.subscribe(() => {
|
||||||
this.currentRoute = this.router.url.toString().substring(1);
|
const urlTree = this.router.parseUrl(this.router.url);
|
||||||
// this.initializeTheme();
|
const urlSegmentGroup = urlTree.root.children[PRIMARY_OUTLET];
|
||||||
|
const urlSegments = urlSegmentGroup.segments;
|
||||||
|
this.currentRoute = urlSegments[0].path;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.tokenStorageService
|
this.tokenStorageService
|
||||||
@ -74,7 +76,7 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
this.canCreateAccount = hasPermission(
|
this.canCreateAccount = hasPermission(
|
||||||
this.user.permissions,
|
this.user.permissions,
|
||||||
permissions.createAccount
|
permissions.createUserAccount
|
||||||
);
|
);
|
||||||
|
|
||||||
this.cd.markForCheck();
|
this.cd.markForCheck();
|
||||||
@ -85,6 +87,16 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onCreateAccount() {
|
||||||
|
this.tokenStorageService.signOut();
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
|
}
|
||||||
|
|
||||||
private initializeTheme() {
|
private initializeTheme() {
|
||||||
this.materialCssVarsService.setDarkTheme(
|
this.materialCssVarsService.setDarkTheme(
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').matches
|
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
@ -97,14 +109,4 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
this.materialCssVarsService.setPrimaryColor(primaryColorHex);
|
this.materialCssVarsService.setPrimaryColor(primaryColorHex);
|
||||||
this.materialCssVarsService.setAccentColor(secondaryColorHex);
|
this.materialCssVarsService.setAccentColor(secondaryColorHex);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onCreateAccount() {
|
|
||||||
this.tokenStorageService.signOut();
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ngOnDestroy() {
|
|
||||||
this.unsubscribeSubject.next();
|
|
||||||
this.unsubscribeSubject.complete();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
<table mat-table [dataSource]="dataSource" class="w-100">
|
<table class="gf-table w-100" mat-table [dataSource]="dataSource">
|
||||||
<ng-container matColumnDef="granteeAlias">
|
<ng-container matColumnDef="granteeAlias">
|
||||||
<th mat-header-cell *matHeaderCellDef i18n>User</th>
|
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>User</th>
|
||||||
<td mat-cell *matCellDef="let element">
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
{{ element.granteeAlias }}
|
{{ element.granteeAlias }}
|
||||||
</td></ng-container
|
</td></ng-container
|
||||||
>
|
>
|
||||||
|
|
||||||
<ng-container matColumnDef="type">
|
<ng-container matColumnDef="type">
|
||||||
<th mat-header-cell *matHeaderCellDef i18n>Type</th>
|
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Type</th>
|
||||||
<td mat-cell *matCellDef="let element">
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
<ion-icon class="mr-1" name="lock-closed-outline"></ion-icon>
|
<ion-icon class="mr-1" name="lock-closed-outline"></ion-icon>
|
||||||
Restricted Access
|
Restricted Access
|
||||||
</td></ng-container
|
</td></ng-container
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
@import '~apps/client/src/styles/ghostfolio-style';
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,81 @@
|
|||||||
|
<table
|
||||||
|
class="gf-table w-100"
|
||||||
|
matSort
|
||||||
|
matSortActive="account"
|
||||||
|
matSortDirection="desc"
|
||||||
|
mat-table
|
||||||
|
[dataSource]="dataSource"
|
||||||
|
>
|
||||||
|
<ng-container matColumnDef="account">
|
||||||
|
<th *matHeaderCellDef class="px-1" i18n mat-header-cell mat-sort-header>
|
||||||
|
Name
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
|
{{ element.name }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="platform">
|
||||||
|
<th *matHeaderCellDef class="px-1" i18n mat-header-cell mat-sort-header>
|
||||||
|
Platform
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
|
<div class="d-flex">
|
||||||
|
<gf-symbol-icon
|
||||||
|
*ngIf="element.Platform?.url"
|
||||||
|
class="mr-1"
|
||||||
|
[tooltip]=""
|
||||||
|
[url]="element.Platform?.url"
|
||||||
|
></gf-symbol-icon>
|
||||||
|
<span>{{ element.Platform?.name }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="actions">
|
||||||
|
<th *matHeaderCellDef class="px-1 text-center" i18n mat-header-cell></th>
|
||||||
|
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
||||||
|
<button
|
||||||
|
class="mx-1 no-min-width px-2"
|
||||||
|
mat-button
|
||||||
|
[matMenuTriggerFor]="accountMenu"
|
||||||
|
(click)="$event.stopPropagation()"
|
||||||
|
>
|
||||||
|
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||||
|
</button>
|
||||||
|
<mat-menu #accountMenu="matMenu" xPosition="before">
|
||||||
|
<button i18n mat-menu-item (click)="onUpdateAccount(element)">
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
i18n
|
||||||
|
mat-menu-item
|
||||||
|
[disabled]="element.isDefault || element.Order?.length > 0"
|
||||||
|
(click)="onDeleteAccount(element.id)"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</mat-menu>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="transactions">
|
||||||
|
<th *matHeaderCellDef i18n mat-header-cell mat-sort-header>Transactions</th>
|
||||||
|
<td *matCellDef="let element" mat-cell>
|
||||||
|
{{ element.Order?.length }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
||||||
|
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<ngx-skeleton-loader
|
||||||
|
*ngIf="isLoading"
|
||||||
|
animation="pulse"
|
||||||
|
class="px-4 py-3"
|
||||||
|
[theme]="{
|
||||||
|
height: '1.5rem',
|
||||||
|
width: '100%'
|
||||||
|
}"
|
||||||
|
></ngx-skeleton-loader>
|
@ -0,0 +1,27 @@
|
|||||||
|
@import '~apps/client/src/styles/ghostfolio-style';
|
||||||
|
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
::ng-deep {
|
||||||
|
.mat-form-field-infix {
|
||||||
|
border-top: 0 solid transparent !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-table {
|
||||||
|
th {
|
||||||
|
::ng-deep {
|
||||||
|
.mat-sort-header-container {
|
||||||
|
justify-content: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(.is-dark-theme) {
|
||||||
|
.mat-form-field {
|
||||||
|
color: rgba(var(--light-primary-text));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
OnChanges,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
Output,
|
||||||
|
ViewChild
|
||||||
|
} from '@angular/core';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { MatSort } from '@angular/material/sort';
|
||||||
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { Account as AccountModel } from '@prisma/client';
|
||||||
|
import { Subject, Subscription } from 'rxjs';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'gf-accounts-table',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
templateUrl: './accounts-table.component.html',
|
||||||
|
styleUrls: ['./accounts-table.component.scss']
|
||||||
|
})
|
||||||
|
export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
|
||||||
|
@Input() accounts: AccountModel[];
|
||||||
|
@Input() baseCurrency: string;
|
||||||
|
@Input() deviceType: string;
|
||||||
|
@Input() locale: string;
|
||||||
|
@Input() showActions: boolean;
|
||||||
|
|
||||||
|
@Output() accountDeleted = new EventEmitter<string>();
|
||||||
|
@Output() accountToUpdate = new EventEmitter<AccountModel>();
|
||||||
|
|
||||||
|
@ViewChild(MatSort) sort: MatSort;
|
||||||
|
|
||||||
|
public dataSource: MatTableDataSource<AccountModel> = new MatTableDataSource();
|
||||||
|
public displayedColumns = [];
|
||||||
|
public isLoading = true;
|
||||||
|
public routeQueryParams: Subscription;
|
||||||
|
|
||||||
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private dialog: MatDialog,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private router: Router
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public ngOnInit() {}
|
||||||
|
|
||||||
|
public ngOnChanges() {
|
||||||
|
this.displayedColumns = ['account', 'platform', 'transactions'];
|
||||||
|
|
||||||
|
if (this.showActions) {
|
||||||
|
this.displayedColumns.push('actions');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
|
if (this.accounts) {
|
||||||
|
this.dataSource = new MatTableDataSource(this.accounts);
|
||||||
|
this.dataSource.sort = this.sort;
|
||||||
|
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public onDeleteAccount(aId: string) {
|
||||||
|
const confirmation = confirm('Do you really want to delete this account?');
|
||||||
|
|
||||||
|
if (confirmation) {
|
||||||
|
this.accountDeleted.emit(aId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public onUpdateAccount(aAccount: AccountModel) {
|
||||||
|
this.accountToUpdate.emit(aAccount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
|
import { MatSortModule } from '@angular/material/sort';
|
||||||
|
import { MatTableModule } from '@angular/material/table';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
|
import { GfSymbolIconModule } from '../symbol-icon/symbol-icon.module';
|
||||||
|
import { GfValueModule } from '../value/value.module';
|
||||||
|
import { AccountsTableComponent } from './accounts-table.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [AccountsTableComponent],
|
||||||
|
exports: [AccountsTableComponent],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
GfSymbolIconModule,
|
||||||
|
GfValueModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatMenuModule,
|
||||||
|
MatSortModule,
|
||||||
|
MatTableModule,
|
||||||
|
NgxSkeletonLoaderModule,
|
||||||
|
RouterModule
|
||||||
|
],
|
||||||
|
providers: [],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
})
|
||||||
|
export class GfAccountsTableModule {}
|
@ -6,59 +6,76 @@
|
|||||||
<span class="spacer"></span>
|
<span class="spacer"></span>
|
||||||
<a
|
<a
|
||||||
class="d-none d-sm-block"
|
class="d-none d-sm-block"
|
||||||
[routerLink]="['/']"
|
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[color]="currentRoute === 'home' ? 'primary' : null"
|
[color]="currentRoute === 'home' ? 'primary' : null"
|
||||||
|
[routerLink]="['/']"
|
||||||
>Overview</a
|
>Overview</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
class="d-none d-sm-block mx-1"
|
class="d-none d-sm-block mx-1"
|
||||||
[routerLink]="['/analysis']"
|
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[color]="currentRoute === 'analysis' ? 'primary' : null"
|
[color]="currentRoute === 'analysis' ? 'primary' : null"
|
||||||
|
[routerLink]="['/analysis']"
|
||||||
>Analysis</a
|
>Analysis</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
class="d-none d-sm-block mx-1"
|
class="d-none d-sm-block mx-1"
|
||||||
[routerLink]="['/report']"
|
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[color]="currentRoute === 'report' ? 'primary' : null"
|
[color]="currentRoute === 'report' ? 'primary' : null"
|
||||||
|
[routerLink]="['/report']"
|
||||||
>X-ray</a
|
>X-ray</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
class="d-none d-sm-block mx-1"
|
class="d-none d-sm-block mx-1"
|
||||||
[routerLink]="['/transactions']"
|
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[color]="currentRoute === 'transactions' ? 'primary' : null"
|
[color]="currentRoute === 'transactions' ? 'primary' : null"
|
||||||
|
[routerLink]="['/transactions']"
|
||||||
>Transactions</a
|
>Transactions</a
|
||||||
>
|
>
|
||||||
|
<a
|
||||||
|
class="d-none d-sm-block mx-1"
|
||||||
|
i18n
|
||||||
|
mat-flat-button
|
||||||
|
[color]="currentRoute === 'accounts' ? 'primary' : null"
|
||||||
|
[routerLink]="['/accounts']"
|
||||||
|
>Accounts</a
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
*ngIf="hasPermissionToAccessAdminControl"
|
*ngIf="hasPermissionToAccessAdminControl"
|
||||||
class="d-none d-sm-block mx-1"
|
class="d-none d-sm-block mx-1"
|
||||||
[routerLink]="['/admin']"
|
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[color]="currentRoute === 'admin' ? 'primary' : null"
|
[color]="currentRoute === 'admin' ? 'primary' : null"
|
||||||
|
[routerLink]="['/admin']"
|
||||||
>Admin Control</a
|
>Admin Control</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
class="d-none d-sm-block mx-1"
|
class="d-none d-sm-block mx-1"
|
||||||
[routerLink]="['/resources']"
|
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[color]="currentRoute === 'resources' ? 'primary' : null"
|
[color]="currentRoute === 'resources' ? 'primary' : null"
|
||||||
|
[routerLink]="['/resources']"
|
||||||
>Resources</a
|
>Resources</a
|
||||||
>
|
>
|
||||||
|
<a
|
||||||
|
*ngIf="hasPermissionForSubscription"
|
||||||
|
class="d-none d-sm-block mx-1"
|
||||||
|
i18n
|
||||||
|
mat-flat-button
|
||||||
|
[color]="currentRoute === 'pricing' ? 'primary' : null"
|
||||||
|
[routerLink]="['/pricing']"
|
||||||
|
>Pricing</a
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
class="d-none d-sm-block mx-1"
|
class="d-none d-sm-block mx-1"
|
||||||
[routerLink]="['/about']"
|
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[color]="currentRoute === 'about' ? 'primary' : null"
|
[color]="currentRoute === 'about' ? 'primary' : null"
|
||||||
|
[routerLink]="['/about']"
|
||||||
>About</a
|
>About</a
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@ -119,60 +136,81 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
<a
|
<a
|
||||||
class="d-block d-sm-none"
|
class="d-block d-sm-none"
|
||||||
[routerLink]="['/analysis']"
|
|
||||||
i18n
|
i18n
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'analysis' }"
|
[ngClass]="{ 'font-weight-bold': currentRoute === 'analysis' }"
|
||||||
|
[routerLink]="['/analysis']"
|
||||||
>Analysis</a
|
>Analysis</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
class="d-block d-sm-none"
|
class="d-block d-sm-none"
|
||||||
[routerLink]="['/report']"
|
|
||||||
i18n
|
i18n
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'report' }"
|
[ngClass]="{ 'font-weight-bold': currentRoute === 'report' }"
|
||||||
|
[routerLink]="['/report']"
|
||||||
>X-ray</a
|
>X-ray</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
class="d-block d-sm-none"
|
class="d-block d-sm-none"
|
||||||
[routerLink]="['/transactions']"
|
|
||||||
i18n
|
i18n
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'transactions' }"
|
[ngClass]="{
|
||||||
|
'font-weight-bold': currentRoute === 'transactions'
|
||||||
|
}"
|
||||||
|
[routerLink]="['/transactions']"
|
||||||
>Transactions</a
|
>Transactions</a
|
||||||
>
|
>
|
||||||
|
<a
|
||||||
|
class="d-block d-sm-none"
|
||||||
|
i18n
|
||||||
|
mat-menu-item
|
||||||
|
[ngClass]="{ 'font-weight-bold': currentRoute === 'accounts' }"
|
||||||
|
[routerLink]="['/accounts']"
|
||||||
|
>Accounts</a
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
class="align-items-center d-flex"
|
class="align-items-center d-flex"
|
||||||
[routerLink]="['/account']"
|
|
||||||
i18n
|
i18n
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'account' }"
|
[ngClass]="{ 'font-weight-bold': currentRoute === 'account' }"
|
||||||
>Account</a
|
[routerLink]="['/account']"
|
||||||
|
>Ghostfolio Account</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
*ngIf="hasPermissionToAccessAdminControl"
|
*ngIf="hasPermissionToAccessAdminControl"
|
||||||
class="d-block d-sm-none"
|
class="d-block d-sm-none"
|
||||||
[routerLink]="['/admin']"
|
|
||||||
i18n
|
i18n
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'admin' }"
|
[ngClass]="{ 'font-weight-bold': currentRoute === 'admin' }"
|
||||||
|
[routerLink]="['/admin']"
|
||||||
>Admin Control</a
|
>Admin Control</a
|
||||||
>
|
>
|
||||||
<hr class="m-0" />
|
<hr class="m-0" />
|
||||||
<a
|
<a
|
||||||
class="d-block d-sm-none"
|
class="d-block d-sm-none"
|
||||||
[routerLink]="['/resources']"
|
|
||||||
i18n
|
i18n
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'resources' }"
|
[ngClass]="{
|
||||||
|
'font-weight-bold': currentRoute === 'resources'
|
||||||
|
}"
|
||||||
|
[routerLink]="['/resources']"
|
||||||
>Resources</a
|
>Resources</a
|
||||||
>
|
>
|
||||||
|
<a
|
||||||
|
*ngIf="hasPermissionForSubscription"
|
||||||
|
class="d-block d-sm-none"
|
||||||
|
i18n
|
||||||
|
mat-menu-item
|
||||||
|
[ngClass]="{ 'font-weight-bold': currentRoute === 'pricing' }"
|
||||||
|
[routerLink]="['/pricing']"
|
||||||
|
>Pricing</a
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
class="d-block d-sm-none"
|
class="d-block d-sm-none"
|
||||||
[routerLink]="['/about']"
|
|
||||||
i18n
|
i18n
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'about' }"
|
[ngClass]="{ 'font-weight-bold': currentRoute === 'about' }"
|
||||||
|
[routerLink]="['/about']"
|
||||||
>About Ghostfolio</a
|
>About Ghostfolio</a
|
||||||
>
|
>
|
||||||
<hr class="d-block d-sm-none m-0" />
|
<hr class="d-block d-sm-none m-0" />
|
||||||
@ -182,19 +220,27 @@
|
|||||||
<ng-container *ngIf="user === null">
|
<ng-container *ngIf="user === null">
|
||||||
<a
|
<a
|
||||||
*ngIf="currentRoute && currentRoute !== 'start'"
|
*ngIf="currentRoute && currentRoute !== 'start'"
|
||||||
[routerLink]="['/']"
|
|
||||||
class="mx-2 no-min-width px-2"
|
class="mx-2 no-min-width px-2"
|
||||||
mat-button
|
mat-button
|
||||||
|
[routerLink]="['/']"
|
||||||
>
|
>
|
||||||
<gf-logo></gf-logo>
|
<gf-logo></gf-logo>
|
||||||
</a>
|
</a>
|
||||||
<span class="spacer"></span>
|
<span class="spacer"></span>
|
||||||
|
<a
|
||||||
|
*ngIf="hasPermissionForSubscription"
|
||||||
|
i18n
|
||||||
|
mat-flat-button
|
||||||
|
[color]="currentRoute === 'pricing' ? 'primary' : null"
|
||||||
|
[routerLink]="['/pricing']"
|
||||||
|
>Pricing</a
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
class="d-none d-sm-block mx-1"
|
class="d-none d-sm-block mx-1"
|
||||||
[routerLink]="['/about']"
|
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[color]="currentRoute === 'about' ? 'primary' : null"
|
[color]="currentRoute === 'about' ? 'primary' : null"
|
||||||
|
[routerLink]="['/about']"
|
||||||
>About</a
|
>About</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
|
@ -27,8 +27,9 @@ export class HeaderComponent implements OnChanges {
|
|||||||
@Input() info: InfoItem;
|
@Input() info: InfoItem;
|
||||||
@Input() user: User;
|
@Input() user: User;
|
||||||
|
|
||||||
public hasPermissionToAccessAdminControl: boolean;
|
|
||||||
public hasPermissionForSocialLogin: boolean;
|
public hasPermissionForSocialLogin: boolean;
|
||||||
|
public hasPermissionForSubscription: boolean;
|
||||||
|
public hasPermissionToAccessAdminControl: boolean;
|
||||||
public impersonationId: string;
|
public impersonationId: string;
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
@ -48,17 +49,20 @@ export class HeaderComponent implements OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ngOnChanges() {
|
public ngOnChanges() {
|
||||||
if (this.user) {
|
|
||||||
this.hasPermissionToAccessAdminControl = hasPermission(
|
|
||||||
this.user.permissions,
|
|
||||||
permissions.accessAdminControl
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.hasPermissionForSocialLogin = hasPermission(
|
this.hasPermissionForSocialLogin = hasPermission(
|
||||||
this.info?.globalPermissions,
|
this.info?.globalPermissions,
|
||||||
permissions.enableSocialLogin
|
permissions.enableSocialLogin
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.hasPermissionForSubscription = hasPermission(
|
||||||
|
this.info?.globalPermissions,
|
||||||
|
permissions.enableSubscription
|
||||||
|
);
|
||||||
|
|
||||||
|
this.hasPermissionToAccessAdminControl = hasPermission(
|
||||||
|
this.user?.permissions,
|
||||||
|
permissions.accessAdminControl
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public impersonateAccount(aId: string) {
|
public impersonateAccount(aId: string) {
|
||||||
|
@ -9,7 +9,11 @@ import {
|
|||||||
OnInit,
|
OnInit,
|
||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/helper';
|
import {
|
||||||
|
getBackgroundColor,
|
||||||
|
primaryColorRgb,
|
||||||
|
secondaryColorRgb
|
||||||
|
} from '@ghostfolio/helper';
|
||||||
import {
|
import {
|
||||||
Chart,
|
Chart,
|
||||||
Filler,
|
Filler,
|
||||||
@ -62,6 +66,10 @@ export class LineChartComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.chart?.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
private initialize() {
|
private initialize() {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
const benchmarkPrices = [];
|
const benchmarkPrices = [];
|
||||||
@ -76,7 +84,7 @@ export class LineChartComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
|
|
||||||
const canvas = document.getElementById('chartCanvas');
|
const canvas = document.getElementById('chartCanvas');
|
||||||
|
|
||||||
var gradient = this.chartCanvas?.nativeElement
|
const gradient = this.chartCanvas?.nativeElement
|
||||||
?.getContext('2d')
|
?.getContext('2d')
|
||||||
.createLinearGradient(
|
.createLinearGradient(
|
||||||
0,
|
0,
|
||||||
@ -88,14 +96,7 @@ export class LineChartComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
0,
|
0,
|
||||||
`rgba(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b}, 0.01)`
|
`rgba(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b}, 0.01)`
|
||||||
);
|
);
|
||||||
gradient.addColorStop(
|
gradient.addColorStop(1, getBackgroundColor());
|
||||||
1,
|
|
||||||
getComputedStyle(document.documentElement).getPropertyValue(
|
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
||||||
? '--dark-background'
|
|
||||||
: '--light-background'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
labels,
|
labels,
|
||||||
@ -181,8 +182,4 @@ export class LineChartComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
|
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
|
||||||
this.chart?.destroy();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
|
import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
|
||||||
|
import { UNKNOWN_KEY, getCssVariable, getTextColor } from '@ghostfolio/helper';
|
||||||
import { Currency } from '@prisma/client';
|
import { Currency } from '@prisma/client';
|
||||||
import { Tooltip } from 'chart.js';
|
import { Tooltip } from 'chart.js';
|
||||||
import { LinearScale } from 'chart.js';
|
import { LinearScale } from 'chart.js';
|
||||||
@ -36,6 +37,14 @@ export class PortfolioProportionChartComponent
|
|||||||
public chart: Chart;
|
public chart: Chart;
|
||||||
public isLoading = true;
|
public isLoading = true;
|
||||||
|
|
||||||
|
private colorMap: {
|
||||||
|
[symbol: string]: string;
|
||||||
|
} = {
|
||||||
|
[UNKNOWN_KEY]: `rgba(${getTextColor()}, ${getCssVariable(
|
||||||
|
'--palette-foreground-divider-alpha'
|
||||||
|
)})`
|
||||||
|
};
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
Chart.register(ArcElement, DoughnutController, LinearScale, Tooltip);
|
Chart.register(ArcElement, DoughnutController, LinearScale, Tooltip);
|
||||||
}
|
}
|
||||||
@ -48,37 +57,67 @@ export class PortfolioProportionChartComponent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.chart?.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
private initialize() {
|
private initialize() {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
const chartData: { [symbol: string]: number } = {};
|
const chartData: {
|
||||||
|
[symbol: string]: { color?: string; value: number };
|
||||||
|
} = {};
|
||||||
|
|
||||||
Object.keys(this.positions).forEach((symbol) => {
|
Object.keys(this.positions).forEach((symbol) => {
|
||||||
if (this.positions[symbol][this.key]) {
|
if (this.positions[symbol][this.key]) {
|
||||||
if (chartData[this.positions[symbol][this.key]]) {
|
if (chartData[this.positions[symbol][this.key]]) {
|
||||||
chartData[this.positions[symbol][this.key]] += this.positions[
|
chartData[this.positions[symbol][this.key]].value += this.positions[
|
||||||
symbol
|
symbol
|
||||||
].value;
|
].value;
|
||||||
} else {
|
} else {
|
||||||
chartData[this.positions[symbol][this.key]] = this.positions[
|
chartData[this.positions[symbol][this.key]] = {
|
||||||
symbol
|
value: this.positions[symbol].value
|
||||||
].value;
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (chartData[UNKNOWN_KEY]) {
|
||||||
|
chartData[UNKNOWN_KEY].value += this.positions[symbol].value;
|
||||||
|
} else {
|
||||||
|
chartData[UNKNOWN_KEY] = {
|
||||||
|
value: this.positions[symbol].value
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const chartDataSorted = Object.entries(chartData)
|
const chartDataSorted = Object.entries(chartData)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
return a[1] - b[1];
|
return a[1].value - b[1].value;
|
||||||
})
|
})
|
||||||
.reverse();
|
.reverse();
|
||||||
|
|
||||||
|
chartDataSorted.forEach(([symbol, item], index) => {
|
||||||
|
if (this.colorMap[symbol]) {
|
||||||
|
// Reuse color
|
||||||
|
item.color = this.colorMap[symbol];
|
||||||
|
} else {
|
||||||
|
const color = this.getColorPalette()[index];
|
||||||
|
|
||||||
|
// Store color for reuse
|
||||||
|
this.colorMap[symbol] = color;
|
||||||
|
|
||||||
|
item.color = color;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
backgroundColor: this.getColorPalette(),
|
backgroundColor: chartDataSorted.map(([, item]) => {
|
||||||
|
return item.color;
|
||||||
|
}),
|
||||||
borderWidth: 0,
|
borderWidth: 0,
|
||||||
data: chartDataSorted.map(([, value]) => {
|
data: chartDataSorted.map(([, item]) => {
|
||||||
return value;
|
return item.value;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -100,21 +139,14 @@ export class PortfolioProportionChartComponent
|
|||||||
tooltip: {
|
tooltip: {
|
||||||
callbacks: {
|
callbacks: {
|
||||||
label: (context) => {
|
label: (context) => {
|
||||||
const label = data.labels[context.dataIndex];
|
const label =
|
||||||
|
context.label === UNKNOWN_KEY ? 'Other' : context.label;
|
||||||
|
|
||||||
if (this.isInPercent) {
|
if (this.isInPercent) {
|
||||||
const value =
|
const value = 100 * <number>context.raw;
|
||||||
100 *
|
|
||||||
data.datasets[context.datasetIndex].data[
|
|
||||||
context.dataIndex
|
|
||||||
];
|
|
||||||
return `${label} (${value.toFixed(2)}%)`;
|
return `${label} (${value.toFixed(2)}%)`;
|
||||||
} else {
|
} else {
|
||||||
const value =
|
const value = <number>context.raw;
|
||||||
data.datasets[context.datasetIndex].data[
|
|
||||||
context.dataIndex
|
|
||||||
];
|
|
||||||
|
|
||||||
return `${label} (${value.toLocaleString(this.locale, {
|
return `${label} (${value.toLocaleString(this.locale, {
|
||||||
maximumFractionDigits: 2,
|
maximumFractionDigits: 2,
|
||||||
minimumFractionDigits: 2
|
minimumFractionDigits: 2
|
||||||
@ -153,8 +185,4 @@ export class PortfolioProportionChartComponent
|
|||||||
'#cc5de8' // grape 5
|
'#cc5de8' // grape 5
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
|
||||||
this.chart?.destroy();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ export class PositionDetailDialog {
|
|||||||
public benchmarkDataItems: LineChartItem[];
|
public benchmarkDataItems: LineChartItem[];
|
||||||
public currency: string;
|
public currency: string;
|
||||||
public firstBuyDate: string;
|
public firstBuyDate: string;
|
||||||
|
public grossPerformance: number;
|
||||||
public grossPerformancePercent: number;
|
public grossPerformancePercent: number;
|
||||||
public historicalDataItems: LineChartItem[];
|
public historicalDataItems: LineChartItem[];
|
||||||
public investment: number;
|
public investment: number;
|
||||||
@ -30,6 +31,7 @@ export class PositionDetailDialog {
|
|||||||
public maxPrice: number;
|
public maxPrice: number;
|
||||||
public minPrice: number;
|
public minPrice: number;
|
||||||
public quantity: number;
|
public quantity: number;
|
||||||
|
public transactionCount: number;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private cd: ChangeDetectorRef,
|
private cd: ChangeDetectorRef,
|
||||||
@ -44,18 +46,21 @@ export class PositionDetailDialog {
|
|||||||
averagePrice,
|
averagePrice,
|
||||||
currency,
|
currency,
|
||||||
firstBuyDate,
|
firstBuyDate,
|
||||||
|
grossPerformance,
|
||||||
grossPerformancePercent,
|
grossPerformancePercent,
|
||||||
historicalData,
|
historicalData,
|
||||||
investment,
|
investment,
|
||||||
marketPrice,
|
marketPrice,
|
||||||
maxPrice,
|
maxPrice,
|
||||||
minPrice,
|
minPrice,
|
||||||
quantity
|
quantity,
|
||||||
|
transactionCount
|
||||||
}) => {
|
}) => {
|
||||||
this.averagePrice = averagePrice;
|
this.averagePrice = averagePrice;
|
||||||
this.benchmarkDataItems = [];
|
this.benchmarkDataItems = [];
|
||||||
this.currency = currency;
|
this.currency = currency;
|
||||||
this.firstBuyDate = firstBuyDate;
|
this.firstBuyDate = firstBuyDate;
|
||||||
|
this.grossPerformance = quantity * grossPerformance;
|
||||||
this.grossPerformancePercent = grossPerformancePercent;
|
this.grossPerformancePercent = grossPerformancePercent;
|
||||||
this.historicalDataItems = historicalData.map(
|
this.historicalDataItems = historicalData.map(
|
||||||
(historicalDataItem) => {
|
(historicalDataItem) => {
|
||||||
@ -75,6 +80,7 @@ export class PositionDetailDialog {
|
|||||||
this.maxPrice = maxPrice;
|
this.maxPrice = maxPrice;
|
||||||
this.minPrice = minPrice;
|
this.minPrice = minPrice;
|
||||||
this.quantity = quantity;
|
this.quantity = quantity;
|
||||||
|
this.transactionCount = transactionCount;
|
||||||
|
|
||||||
if (isToday(parseISO(this.firstBuyDate))) {
|
if (isToday(parseISO(this.firstBuyDate))) {
|
||||||
// Add average price
|
// Add average price
|
||||||
|
@ -19,6 +19,16 @@
|
|||||||
></gf-line-chart>
|
></gf-line-chart>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
<div class="col-6 mb-3">
|
||||||
|
<gf-value
|
||||||
|
label="Change"
|
||||||
|
size="medium"
|
||||||
|
[colorizeSign]="true"
|
||||||
|
[currency]="data.baseCurrency"
|
||||||
|
[locale]="data.locale"
|
||||||
|
[value]="grossPerformance"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
<gf-value
|
<gf-value
|
||||||
label="Performance"
|
label="Performance"
|
||||||
@ -29,13 +39,6 @@
|
|||||||
[value]="grossPerformancePercent"
|
[value]="grossPerformancePercent"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 mb-3">
|
|
||||||
<gf-value
|
|
||||||
label="First Buy Date"
|
|
||||||
size="medium"
|
|
||||||
[value]="firstBuyDate"
|
|
||||||
></gf-value>
|
|
||||||
</div>
|
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
<gf-value
|
<gf-value
|
||||||
label="Ø Buy Price"
|
label="Ø Buy Price"
|
||||||
@ -91,6 +94,22 @@
|
|||||||
[value]="investment"
|
[value]="investment"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-6 mb-3">
|
||||||
|
<gf-value
|
||||||
|
label="First Buy Date"
|
||||||
|
size="medium"
|
||||||
|
[value]="firstBuyDate"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 mb-3">
|
||||||
|
<gf-value
|
||||||
|
label="Transactions"
|
||||||
|
size="medium"
|
||||||
|
[isCurrency]="true"
|
||||||
|
[locale]="data.locale"
|
||||||
|
[value]="transactionCount"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,31 +1,30 @@
|
|||||||
<!--<mat-form-field appearance="outline" class="w-100">
|
|
||||||
<input #input autocomplete="off" matInput (keyup)="applyFilter($event)" />
|
|
||||||
<ion-icon class="mr-1" matPrefix name="search-outline"></ion-icon>
|
|
||||||
</mat-form-field>-->
|
|
||||||
|
|
||||||
<table
|
<table
|
||||||
class="w-100"
|
class="gf-table w-100"
|
||||||
matSort
|
matSort
|
||||||
matSortActive="shareCurrent"
|
matSortActive="allocationCurrent"
|
||||||
matSortDirection="desc"
|
matSortDirection="desc"
|
||||||
mat-table
|
mat-table
|
||||||
[dataSource]="dataSource"
|
[dataSource]="dataSource"
|
||||||
>
|
>
|
||||||
<ng-container matColumnDef="symbol">
|
<ng-container matColumnDef="symbol">
|
||||||
<th mat-header-cell *matHeaderCellDef mat-sort-header i18n>Symbol</th>
|
<th *matHeaderCellDef class="px-1" i18n mat-header-cell mat-sort-header>
|
||||||
<td mat-cell *matCellDef="let element">{{ element.symbol | gfSymbol }}</td>
|
Symbol
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
|
{{ element.symbol | gfSymbol }}
|
||||||
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="performance">
|
<ng-container matColumnDef="performance">
|
||||||
<th
|
<th
|
||||||
*matHeaderCellDef
|
*matHeaderCellDef
|
||||||
class="d-none d-lg-table-cell text-right"
|
class="d-none d-lg-table-cell px-1 text-right"
|
||||||
i18n
|
i18n
|
||||||
mat-header-cell
|
mat-header-cell
|
||||||
>
|
>
|
||||||
Performance
|
Performance
|
||||||
</th>
|
</th>
|
||||||
<td class="d-none d-lg-table-cell" mat-cell *matCellDef="let element">
|
<td class="d-none d-lg-table-cell px-1" mat-cell *matCellDef="let element">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<gf-value
|
<gf-value
|
||||||
[colorizeSign]="true"
|
[colorizeSign]="true"
|
||||||
@ -37,43 +36,43 @@
|
|||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="shareInvestment">
|
<ng-container matColumnDef="allocationInvestment">
|
||||||
<th
|
<th
|
||||||
*matHeaderCellDef
|
*matHeaderCellDef
|
||||||
class="justify-content-end"
|
class="justify-content-end px-1"
|
||||||
i18n
|
i18n
|
||||||
mat-header-cell
|
mat-header-cell
|
||||||
mat-sort-header
|
mat-sort-header
|
||||||
>
|
>
|
||||||
Initial Share
|
Initial Allocation
|
||||||
</th>
|
</th>
|
||||||
<td mat-cell *matCellDef="let element">
|
<td mat-cell *matCellDef="let element">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end px-1">
|
||||||
<gf-value
|
<gf-value
|
||||||
[isPercent]="true"
|
[isPercent]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[value]="isLoading ? undefined : element.shareInvestment"
|
[value]="isLoading ? undefined : element.allocationInvestment"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="shareCurrent">
|
<ng-container matColumnDef="allocationCurrent">
|
||||||
<th
|
<th
|
||||||
*matHeaderCellDef
|
*matHeaderCellDef
|
||||||
class="justify-content-end"
|
class="justify-content-end px-1"
|
||||||
i18n
|
i18n
|
||||||
mat-header-cell
|
mat-header-cell
|
||||||
mat-sort-header
|
mat-sort-header
|
||||||
>
|
>
|
||||||
Current Share
|
Current Allocation
|
||||||
</th>
|
</th>
|
||||||
<td mat-cell *matCellDef="let element">
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<gf-value
|
<gf-value
|
||||||
[isPercent]="true"
|
[isPercent]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[value]="isLoading ? undefined : element.shareCurrent"
|
[value]="isLoading ? undefined : element.allocationCurrent"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
@import '~apps/client/src/styles/ghostfolio-style';
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
@ -8,10 +10,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mat-table {
|
.mat-table {
|
||||||
td {
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
th {
|
||||||
::ng-deep {
|
::ng-deep {
|
||||||
.mat-sort-header-container {
|
.mat-sort-header-container {
|
||||||
@ -22,13 +20,6 @@
|
|||||||
|
|
||||||
.mat-row {
|
.mat-row {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:nth-child(even) {
|
|
||||||
background-color: rgba(
|
|
||||||
var(--dark-primary-text),
|
|
||||||
var(--palette-background-hover-alpha)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -37,15 +28,4 @@
|
|||||||
.mat-form-field {
|
.mat-form-field {
|
||||||
color: rgba(var(--light-primary-text));
|
color: rgba(var(--light-primary-text));
|
||||||
}
|
}
|
||||||
|
|
||||||
.mat-table {
|
|
||||||
.mat-row {
|
|
||||||
&:nth-child(even) {
|
|
||||||
background-color: rgba(
|
|
||||||
var(--light-primary-text),
|
|
||||||
var(--palette-background-hover-alpha)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -73,8 +73,8 @@ export class PositionsTableComponent implements OnChanges, OnInit {
|
|||||||
this.displayedColumns = [
|
this.displayedColumns = [
|
||||||
'symbol',
|
'symbol',
|
||||||
'performance',
|
'performance',
|
||||||
'shareInvestment',
|
'allocationInvestment',
|
||||||
'shareCurrent'
|
'allocationCurrent'
|
||||||
];
|
];
|
||||||
|
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
@ -1,52 +1,59 @@
|
|||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
<input
|
|
||||||
#input
|
|
||||||
autocomplete="off"
|
|
||||||
matInput
|
|
||||||
placeholder="Search for transactions..."
|
|
||||||
(keyup)="applyFilter($event)"
|
|
||||||
/>
|
|
||||||
<ion-icon class="mr-1" matPrefix name="search-outline"></ion-icon>
|
<ion-icon class="mr-1" matPrefix name="search-outline"></ion-icon>
|
||||||
|
<mat-chip-list #chipList aria-label="Search keywords">
|
||||||
|
<mat-chip
|
||||||
|
*ngFor="let searchKeyword of searchKeywords"
|
||||||
|
class="mx-1 my-0 px-2 py-0"
|
||||||
|
matChipRemove
|
||||||
|
[removable]="true"
|
||||||
|
(removed)="removeKeyword(searchKeyword)"
|
||||||
|
>
|
||||||
|
{{ searchKeyword }}
|
||||||
|
<ion-icon class="ml-2" matPrefix name="close-outline"></ion-icon>
|
||||||
|
</mat-chip>
|
||||||
|
<input
|
||||||
|
#searchInput
|
||||||
|
name="close-outline"
|
||||||
|
[formControl]="searchControl"
|
||||||
|
[matAutocomplete]="autocomplete"
|
||||||
|
[matChipInputFor]="chipList"
|
||||||
|
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
|
||||||
|
[placeholder]="placeholder"
|
||||||
|
(matChipInputTokenEnd)="addKeyword($event)"
|
||||||
|
/>
|
||||||
|
</mat-chip-list>
|
||||||
|
<mat-autocomplete
|
||||||
|
#autocomplete="matAutocomplete"
|
||||||
|
(optionSelected)="keywordSelected($event)"
|
||||||
|
>
|
||||||
|
<mat-option
|
||||||
|
*ngFor="let transaction of filteredTransactions | async"
|
||||||
|
[value]="transaction"
|
||||||
|
>
|
||||||
|
{{ transaction }}
|
||||||
|
</mat-option>
|
||||||
|
</mat-autocomplete>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<table
|
<table
|
||||||
class="w-100"
|
class="gf-table w-100"
|
||||||
matSort
|
matSort
|
||||||
matSortActive="date"
|
matSortActive="date"
|
||||||
matSortDirection="desc"
|
matSortDirection="desc"
|
||||||
mat-table
|
mat-table
|
||||||
[dataSource]="dataSource"
|
[dataSource]="dataSource"
|
||||||
>
|
>
|
||||||
<ng-container matColumnDef="platform">
|
|
||||||
<th
|
|
||||||
*matHeaderCellDef
|
|
||||||
class="d-none d-lg-table-cell text-center px-0"
|
|
||||||
i18n
|
|
||||||
mat-header-cell
|
|
||||||
>
|
|
||||||
Platform
|
|
||||||
</th>
|
|
||||||
<td *matCellDef="let element" class="d-none d-lg-table-cell px-0" mat-cell>
|
|
||||||
<div class="d-flex justify-content-center">
|
|
||||||
<gf-symbol-icon
|
|
||||||
*ngIf="element.Platform?.url"
|
|
||||||
[tooltip]="element.Platform?.name"
|
|
||||||
[url]="element.Platform?.url"
|
|
||||||
></gf-symbol-icon>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container matColumnDef="date">
|
<ng-container matColumnDef="date">
|
||||||
<th
|
<th
|
||||||
*matHeaderCellDef
|
*matHeaderCellDef
|
||||||
class="justify-content-center"
|
class="justify-content-center px-1"
|
||||||
i18n
|
i18n
|
||||||
mat-header-cell
|
mat-header-cell
|
||||||
mat-sort-header
|
mat-sort-header
|
||||||
>
|
>
|
||||||
Date
|
Date
|
||||||
</th>
|
</th>
|
||||||
<td mat-cell *matCellDef="let element">
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
<div class="d-flex justify-content-center">
|
<div class="d-flex justify-content-center">
|
||||||
{{ element.date | date: defaultDateFormat }}
|
{{ element.date | date: defaultDateFormat }}
|
||||||
</div>
|
</div>
|
||||||
@ -56,14 +63,14 @@
|
|||||||
<ng-container matColumnDef="type">
|
<ng-container matColumnDef="type">
|
||||||
<th
|
<th
|
||||||
*matHeaderCellDef
|
*matHeaderCellDef
|
||||||
class="justify-content-center"
|
class="justify-content-center px-1"
|
||||||
i18n
|
i18n
|
||||||
mat-header-cell
|
mat-header-cell
|
||||||
mat-sort-header
|
mat-sort-header
|
||||||
>
|
>
|
||||||
Type
|
Type
|
||||||
</th>
|
</th>
|
||||||
<td mat-cell *matCellDef="let element" class="text-center">
|
<td mat-cell *matCellDef="let element" class="px-1 text-center">
|
||||||
<div
|
<div
|
||||||
class="d-inline-flex justify-content-center pl-1 pr-2 py-1 type-badge"
|
class="d-inline-flex justify-content-center pl-1 pr-2 py-1 type-badge"
|
||||||
[ngClass]="element.type == 'BUY' ? 'buy' : 'sell'"
|
[ngClass]="element.type == 'BUY' ? 'buy' : 'sell'"
|
||||||
@ -82,21 +89,25 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="symbol">
|
<ng-container matColumnDef="symbol">
|
||||||
<th *matHeaderCellDef i18n mat-header-cell mat-sort-header>Symbol</th>
|
<th *matHeaderCellDef class="px-1" i18n mat-header-cell mat-sort-header>
|
||||||
<td mat-cell *matCellDef="let element">{{ element.symbol | gfSymbol }}</td>
|
Symbol
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
|
{{ element.symbol | gfSymbol }}
|
||||||
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="currency">
|
<ng-container matColumnDef="currency">
|
||||||
<th
|
<th
|
||||||
*matHeaderCellDef
|
*matHeaderCellDef
|
||||||
class="d-none d-lg-table-cell justify-content-center"
|
class="d-none d-lg-table-cell justify-content-center px-1"
|
||||||
mat-header-cell
|
mat-header-cell
|
||||||
i18n
|
i18n
|
||||||
mat-sort-header
|
mat-sort-header
|
||||||
>
|
>
|
||||||
Currency
|
Currency
|
||||||
</th>
|
</th>
|
||||||
<td class="d-none d-lg-table-cell" mat-cell *matCellDef="let element">
|
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
|
||||||
<div class="d-flex justify-content-center">
|
<div class="d-flex justify-content-center">
|
||||||
{{ element.currency }}
|
{{ element.currency }}
|
||||||
</div>
|
</div>
|
||||||
@ -106,14 +117,14 @@
|
|||||||
<ng-container matColumnDef="quantity">
|
<ng-container matColumnDef="quantity">
|
||||||
<th
|
<th
|
||||||
*matHeaderCellDef
|
*matHeaderCellDef
|
||||||
class="d-none d-lg-table-cell justify-content-end"
|
class="d-none d-lg-table-cell justify-content-end px-1"
|
||||||
i18n
|
i18n
|
||||||
mat-header-cell
|
mat-header-cell
|
||||||
mat-sort-header
|
mat-sort-header
|
||||||
>
|
>
|
||||||
Quantity
|
Quantity
|
||||||
</th>
|
</th>
|
||||||
<td *matCellDef="let element" class="d-none d-lg-table-cell" mat-cell>
|
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<gf-value
|
<gf-value
|
||||||
[isCurrency]="true"
|
[isCurrency]="true"
|
||||||
@ -127,14 +138,14 @@
|
|||||||
<ng-container matColumnDef="unitPrice">
|
<ng-container matColumnDef="unitPrice">
|
||||||
<th
|
<th
|
||||||
*matHeaderCellDef
|
*matHeaderCellDef
|
||||||
class="d-none d-lg-table-cell justify-content-end"
|
class="d-none d-lg-table-cell justify-content-end px-1"
|
||||||
i18n
|
i18n
|
||||||
mat-header-cell
|
mat-header-cell
|
||||||
mat-sort-header
|
mat-sort-header
|
||||||
>
|
>
|
||||||
Unit Price
|
Unit Price
|
||||||
</th>
|
</th>
|
||||||
<td *matCellDef="let element" class="d-none d-lg-table-cell" mat-cell>
|
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<gf-value
|
<gf-value
|
||||||
[isCurrency]="true"
|
[isCurrency]="true"
|
||||||
@ -148,14 +159,14 @@
|
|||||||
<ng-container matColumnDef="fee">
|
<ng-container matColumnDef="fee">
|
||||||
<th
|
<th
|
||||||
*matHeaderCellDef
|
*matHeaderCellDef
|
||||||
class="d-none d-lg-table-cell justify-content-end"
|
class="d-none d-lg-table-cell justify-content-end px-1"
|
||||||
i18n
|
i18n
|
||||||
mat-header-cell
|
mat-header-cell
|
||||||
mat-sort-header
|
mat-sort-header
|
||||||
>
|
>
|
||||||
Fee
|
Fee
|
||||||
</th>
|
</th>
|
||||||
<td class="d-none d-lg-table-cell" mat-cell *matCellDef="let element">
|
<td *matCellDef="let element" class="d-none d-lg-table-cell px1" mat-cell>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<gf-value
|
<gf-value
|
||||||
[isCurrency]="true"
|
[isCurrency]="true"
|
||||||
@ -166,9 +177,26 @@
|
|||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="account">
|
||||||
|
<th *matHeaderCellDef class="px-1" i18n mat-header-cell mat-sort-header>
|
||||||
|
Account
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
|
<div class="d-flex">
|
||||||
|
<gf-symbol-icon
|
||||||
|
*ngIf="element.Account?.Platform?.url"
|
||||||
|
class="mr-1"
|
||||||
|
[tooltip]="element.Account?.Platform?.name"
|
||||||
|
[url]="element.Account?.Platform?.url"
|
||||||
|
></gf-symbol-icon>
|
||||||
|
<span class="d-none d-lg-block">{{ element.Account?.name }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="actions">
|
<ng-container matColumnDef="actions">
|
||||||
<th *matHeaderCellDef class="px-0 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-0 text-center" mat-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
|
||||||
@ -181,6 +209,9 @@
|
|||||||
<button i18n mat-menu-item (click)="onUpdateTransaction(element)">
|
<button i18n mat-menu-item (click)="onUpdateTransaction(element)">
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
|
<button i18n mat-menu-item (click)="onCloneTransaction(element)">
|
||||||
|
Clone
|
||||||
|
</button>
|
||||||
<button i18n mat-menu-item (click)="onDeleteTransaction(element.id)">
|
<button i18n mat-menu-item (click)="onDeleteTransaction(element.id)">
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
@import '~apps/client/src/styles/ghostfolio-style';
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
@ -7,11 +9,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mat-table {
|
.mat-chip {
|
||||||
td {
|
cursor: pointer;
|
||||||
border: 0;
|
min-height: 1.5rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mat-table {
|
||||||
th {
|
th {
|
||||||
::ng-deep {
|
::ng-deep {
|
||||||
.mat-sort-header-container {
|
.mat-sort-header-container {
|
||||||
@ -23,13 +26,6 @@
|
|||||||
.mat-row {
|
.mat-row {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:nth-child(even) {
|
|
||||||
background-color: rgba(
|
|
||||||
var(--dark-primary-text),
|
|
||||||
var(--palette-background-hover-alpha)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
.type-badge {
|
.type-badge {
|
||||||
background-color: rgba(var(--dark-primary-text), 0.05);
|
background-color: rgba(var(--dark-primary-text), 0.05);
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
@ -57,17 +53,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mat-table {
|
.mat-table {
|
||||||
.mat-row {
|
.type-badge {
|
||||||
&:nth-child(even) {
|
background-color: rgba(var(--light-primary-text), 0.1);
|
||||||
background-color: rgba(
|
|
||||||
var(--light-primary-text),
|
|
||||||
var(--palette-background-hover-alpha)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
.type-badge {
|
|
||||||
background-color: rgba(var(--light-primary-text), 0.1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
|
import { COMMA, ENTER } from '@angular/cdk/keycodes';
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
|
ElementRef,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
Input,
|
Input,
|
||||||
OnChanges,
|
OnChanges,
|
||||||
@ -9,17 +11,26 @@ import {
|
|||||||
Output,
|
Output,
|
||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
import { FormControl } from '@angular/forms';
|
||||||
|
import {
|
||||||
|
MatAutocomplete,
|
||||||
|
MatAutocompleteSelectedEvent
|
||||||
|
} from '@angular/material/autocomplete';
|
||||||
|
import { MatChipInputEvent } from '@angular/material/chips';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
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 { OrderWithAccount } from '@ghostfolio/api/app/order/interfaces/order-with-account.type';
|
||||||
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/helper';
|
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/helper';
|
||||||
import { Order as OrderModel } from '@prisma/client';
|
import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs';
|
||||||
import { Subject, Subscription } from 'rxjs';
|
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
import { PositionDetailDialog } from '../position/position-detail-dialog/position-detail-dialog.component';
|
import { PositionDetailDialog } from '../position/position-detail-dialog/position-detail-dialog.component';
|
||||||
|
|
||||||
|
const SEARCH_PLACEHOLDER = 'Search for account, currency, symbol or type...';
|
||||||
|
const SEARCH_STRING_SEPARATOR = ',';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'gf-transactions-table',
|
selector: 'gf-transactions-table',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
@ -32,19 +43,31 @@ export class TransactionsTableComponent
|
|||||||
@Input() deviceType: string;
|
@Input() deviceType: string;
|
||||||
@Input() locale: string;
|
@Input() locale: string;
|
||||||
@Input() showActions: boolean;
|
@Input() showActions: boolean;
|
||||||
@Input() transactions: OrderModel[];
|
@Input() transactions: OrderWithAccount[];
|
||||||
|
|
||||||
@Output() transactionDeleted = new EventEmitter<string>();
|
@Output() transactionDeleted = new EventEmitter<string>();
|
||||||
@Output() transactionToUpdate = new EventEmitter<OrderModel>();
|
@Output() transactionToClone = new EventEmitter<OrderWithAccount>();
|
||||||
|
@Output() transactionToUpdate = new EventEmitter<OrderWithAccount>();
|
||||||
|
|
||||||
|
@ViewChild('autocomplete') matAutocomplete: MatAutocomplete;
|
||||||
|
@ViewChild('searchInput') searchInput: ElementRef<HTMLInputElement>;
|
||||||
@ViewChild(MatSort) sort: MatSort;
|
@ViewChild(MatSort) sort: MatSort;
|
||||||
|
|
||||||
public dataSource: MatTableDataSource<OrderModel> = new MatTableDataSource();
|
public dataSource: MatTableDataSource<OrderWithAccount> = new MatTableDataSource();
|
||||||
public defaultDateFormat = DEFAULT_DATE_FORMAT;
|
public defaultDateFormat = DEFAULT_DATE_FORMAT;
|
||||||
public displayedColumns = [];
|
public displayedColumns = [];
|
||||||
|
public filteredTransactions$: Subject<string[]> = new BehaviorSubject([]);
|
||||||
|
public filteredTransactions: Observable<
|
||||||
|
string[]
|
||||||
|
> = this.filteredTransactions$.asObservable();
|
||||||
public isLoading = true;
|
public isLoading = true;
|
||||||
|
public placeholder = '';
|
||||||
public routeQueryParams: Subscription;
|
public routeQueryParams: Subscription;
|
||||||
|
public searchControl = new FormControl();
|
||||||
|
public searchKeywords: string[] = [];
|
||||||
|
public separatorKeysCodes: number[] = [ENTER, COMMA];
|
||||||
|
|
||||||
|
private allFilteredTransactions: string[];
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
@ -62,41 +85,90 @@ export class TransactionsTableComponent
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.searchControl.valueChanges.subscribe((keyword) => {
|
||||||
|
if (keyword) {
|
||||||
|
const filterValue = keyword.toLowerCase();
|
||||||
|
this.filteredTransactions$.next(
|
||||||
|
this.allFilteredTransactions.filter(
|
||||||
|
(filter) => filter.toLowerCase().indexOf(filterValue) === 0
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.filteredTransactions$.next(this.allFilteredTransactions);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public addKeyword({ input, value }: MatChipInputEvent): void {
|
||||||
|
if (value?.trim()) {
|
||||||
|
this.searchKeywords.push(value.trim());
|
||||||
|
this.updateFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the input value
|
||||||
|
if (input) {
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.searchControl.setValue(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeKeyword(keyword: string): void {
|
||||||
|
const index = this.searchKeywords.indexOf(keyword);
|
||||||
|
|
||||||
|
if (index >= 0) {
|
||||||
|
this.searchKeywords.splice(index, 1);
|
||||||
|
this.updateFilter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public keywordSelected(event: MatAutocompleteSelectedEvent): void {
|
||||||
|
this.searchKeywords.push(event.option.viewValue);
|
||||||
|
this.updateFilter();
|
||||||
|
this.searchInput.nativeElement.value = '';
|
||||||
|
this.searchControl.setValue(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnInit() {}
|
public ngOnInit() {}
|
||||||
|
|
||||||
public ngOnChanges() {
|
public ngOnChanges() {
|
||||||
this.displayedColumns = [
|
this.displayedColumns = [
|
||||||
'platform',
|
|
||||||
'date',
|
'date',
|
||||||
'type',
|
'type',
|
||||||
'symbol',
|
'symbol',
|
||||||
'currency',
|
'currency',
|
||||||
'quantity',
|
'quantity',
|
||||||
'unitPrice',
|
'unitPrice',
|
||||||
'fee'
|
'fee',
|
||||||
|
'account'
|
||||||
];
|
];
|
||||||
|
|
||||||
this.isLoading = true;
|
|
||||||
|
|
||||||
if (this.showActions) {
|
if (this.showActions) {
|
||||||
this.displayedColumns.push('actions');
|
this.displayedColumns.push('actions');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
if (this.transactions) {
|
if (this.transactions) {
|
||||||
this.dataSource = new MatTableDataSource(this.transactions);
|
this.dataSource = new MatTableDataSource(this.transactions);
|
||||||
|
this.dataSource.filterPredicate = (data, filter) => {
|
||||||
|
const dataString = this.getFilterableValues(data)
|
||||||
|
.join(' ')
|
||||||
|
.toLowerCase();
|
||||||
|
let contains = true;
|
||||||
|
for (const singleFilter of filter.split(SEARCH_STRING_SEPARATOR)) {
|
||||||
|
contains =
|
||||||
|
contains && dataString.includes(singleFilter.trim().toLowerCase());
|
||||||
|
}
|
||||||
|
return contains;
|
||||||
|
};
|
||||||
this.dataSource.sort = this.sort;
|
this.dataSource.sort = this.sort;
|
||||||
|
this.updateFilter();
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public applyFilter(event: Event) {
|
|
||||||
const filterValue = (event.target as HTMLInputElement).value;
|
|
||||||
this.dataSource.filter = filterValue.trim().toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
public onDeleteTransaction(aId: string) {
|
public onDeleteTransaction(aId: string) {
|
||||||
const confirmation = confirm(
|
const confirmation = confirm(
|
||||||
'Do you really want to delete this transaction?'
|
'Do you really want to delete this transaction?'
|
||||||
@ -119,10 +191,14 @@ export class TransactionsTableComponent
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onUpdateTransaction(aTransaction: OrderModel) {
|
public onUpdateTransaction(aTransaction: OrderWithAccount) {
|
||||||
this.transactionToUpdate.emit(aTransaction);
|
this.transactionToUpdate.emit(aTransaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onCloneTransaction(aTransaction: OrderWithAccount) {
|
||||||
|
this.transactionToClone.emit(aTransaction);
|
||||||
|
}
|
||||||
|
|
||||||
public openPositionDialog({
|
public openPositionDialog({
|
||||||
symbol,
|
symbol,
|
||||||
title
|
title
|
||||||
@ -152,4 +228,51 @@ export class TransactionsTableComponent
|
|||||||
this.unsubscribeSubject.next();
|
this.unsubscribeSubject.next();
|
||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private updateFilter() {
|
||||||
|
this.dataSource.filter = this.searchKeywords.join(SEARCH_STRING_SEPARATOR);
|
||||||
|
const lowercaseSearchKeywords = this.searchKeywords.map((keyword) =>
|
||||||
|
keyword.trim().toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
this.placeholder =
|
||||||
|
lowercaseSearchKeywords.length <= 0 ? SEARCH_PLACEHOLDER : '';
|
||||||
|
|
||||||
|
this.allFilteredTransactions = this.getSearchableFieldValues(
|
||||||
|
this.transactions
|
||||||
|
).filter((item) => {
|
||||||
|
return !lowercaseSearchKeywords.includes(item.trim().toLowerCase());
|
||||||
|
});
|
||||||
|
|
||||||
|
this.filteredTransactions$.next(this.allFilteredTransactions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSearchableFieldValues(transactions: OrderWithAccount[]): string[] {
|
||||||
|
const fieldValues = new Set<string>();
|
||||||
|
|
||||||
|
for (const transaction of transactions) {
|
||||||
|
this.getFilterableValues(transaction, fieldValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...fieldValues]
|
||||||
|
.filter((item) => {
|
||||||
|
return item !== undefined;
|
||||||
|
})
|
||||||
|
.sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFilterableValues(
|
||||||
|
transaction: OrderWithAccount,
|
||||||
|
fieldValues: Set<string> = new Set<string>()
|
||||||
|
): string[] {
|
||||||
|
fieldValues.add(transaction.currency);
|
||||||
|
fieldValues.add(transaction.symbol);
|
||||||
|
fieldValues.add(transaction.type);
|
||||||
|
fieldValues.add(transaction.Account?.name);
|
||||||
|
fieldValues.add(transaction.Account?.Platform?.name);
|
||||||
|
|
||||||
|
return [...fieldValues].filter((item) => {
|
||||||
|
return item !== undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatChipsModule } from '@angular/material/chips';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { MatMenuModule } from '@angular/material/menu';
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
import { MatSortModule } from '@angular/material/sort';
|
import { MatSortModule } from '@angular/material/sort';
|
||||||
@ -23,12 +26,15 @@ import { TransactionsTableComponent } from './transactions-table.component';
|
|||||||
GfSymbolIconModule,
|
GfSymbolIconModule,
|
||||||
GfSymbolModule,
|
GfSymbolModule,
|
||||||
GfValueModule,
|
GfValueModule,
|
||||||
|
MatAutocompleteModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
|
MatChipsModule,
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
MatMenuModule,
|
MatMenuModule,
|
||||||
MatSortModule,
|
MatSortModule,
|
||||||
MatTableModule,
|
MatTableModule,
|
||||||
NgxSkeletonLoaderModule,
|
NgxSkeletonLoaderModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
RouterModule
|
RouterModule
|
||||||
],
|
],
|
||||||
providers: [],
|
providers: [],
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||||
import { InfoItem } from '@ghostfolio/api/app/info/interfaces/info-item.interface';
|
|
||||||
import { User } from '@ghostfolio/api/app/user/interfaces/user.interface';
|
import { User } from '@ghostfolio/api/app/user/interfaces/user.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 { baseCurrency, hasPermission, permissions } from '@ghostfolio/helper';
|
import { baseCurrency } from '@ghostfolio/helper';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
@ -16,7 +15,6 @@ import { environment } from '../../../environments/environment';
|
|||||||
})
|
})
|
||||||
export class AboutPageComponent implements OnInit {
|
export class AboutPageComponent implements OnInit {
|
||||||
public baseCurrency = baseCurrency;
|
public baseCurrency = baseCurrency;
|
||||||
public hasPermissionForSubscription: boolean;
|
|
||||||
public isLoggedIn: boolean;
|
public isLoggedIn: boolean;
|
||||||
public lastPublish = environment.lastPublish;
|
public lastPublish = environment.lastPublish;
|
||||||
public user: User;
|
public user: User;
|
||||||
@ -37,15 +35,6 @@ export class AboutPageComponent implements OnInit {
|
|||||||
* Initializes the controller
|
* Initializes the controller
|
||||||
*/
|
*/
|
||||||
public ngOnInit() {
|
public ngOnInit() {
|
||||||
this.dataService.fetchInfo().subscribe((info) => {
|
|
||||||
this.hasPermissionForSubscription = hasPermission(
|
|
||||||
info.globalPermissions,
|
|
||||||
permissions.enableSubscription
|
|
||||||
);
|
|
||||||
|
|
||||||
this.cd.markForCheck();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.isLoggedIn = !!this.tokenStorageService.getToken();
|
this.isLoggedIn = !!this.tokenStorageService.getToken();
|
||||||
|
|
||||||
if (this.isLoggedIn)
|
if (this.isLoggedIn)
|
||||||
|
@ -61,105 +61,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="hasPermissionForSubscription" class="mb-5 row">
|
|
||||||
<div class="col">
|
|
||||||
<h3 class="mb-3 text-center" i18n>Pricing Plans</h3>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-xs-12 col-md-6">
|
|
||||||
<mat-card class="mb-3">
|
|
||||||
<h4 i18n>Open Source</h4>
|
|
||||||
<p>Host your <strong>Ghostfolio</strong> instance by yourself.</p>
|
|
||||||
<ul class="list-unstyled mb-3">
|
|
||||||
<li class="align-items-center d-flex mb-1">
|
|
||||||
<ion-icon
|
|
||||||
class="mr-1 text-muted"
|
|
||||||
name="checkmark-circle-outline"
|
|
||||||
></ion-icon>
|
|
||||||
<span>Portfolio Performance</span>
|
|
||||||
</li>
|
|
||||||
<li class="align-items-center d-flex mb-1">
|
|
||||||
<ion-icon
|
|
||||||
class="mr-1 text-muted"
|
|
||||||
name="checkmark-circle-outline"
|
|
||||||
></ion-icon>
|
|
||||||
<span>Portfolio Summary</span>
|
|
||||||
</li>
|
|
||||||
<li class="align-items-center d-flex mb-1">
|
|
||||||
<ion-icon
|
|
||||||
class="mr-1 text-muted"
|
|
||||||
name="checkmark-circle-outline"
|
|
||||||
></ion-icon>
|
|
||||||
<span>Unlimited Transactions</span>
|
|
||||||
</li>
|
|
||||||
<li class="align-items-center d-flex mb-1">
|
|
||||||
<ion-icon
|
|
||||||
class="mr-1 text-muted"
|
|
||||||
name="checkmark-circle-outline"
|
|
||||||
></ion-icon>
|
|
||||||
<span>Advanced Insights</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p class="h5 text-right">
|
|
||||||
<span>Free</span>
|
|
||||||
</p>
|
|
||||||
</mat-card>
|
|
||||||
</div>
|
|
||||||
<div class="col-xs-12 col-md-6">
|
|
||||||
<mat-card
|
|
||||||
class="mb-3"
|
|
||||||
[ngClass]="{ 'active': user?.subscription?.type === 'Trial' }"
|
|
||||||
>
|
|
||||||
<h4 class="align-items-center d-flex" i18n>
|
|
||||||
Diamond
|
|
||||||
<ion-icon
|
|
||||||
class="ml-1 text-muted"
|
|
||||||
name="diamond-outline"
|
|
||||||
></ion-icon>
|
|
||||||
</h4>
|
|
||||||
<p>Get a managed <strong>Ghostfolio</strong> cloud offering.</p>
|
|
||||||
<ul class="list-unstyled mb-3">
|
|
||||||
<li class="align-items-center d-flex mb-1">
|
|
||||||
<ion-icon
|
|
||||||
class="mr-1 text-muted"
|
|
||||||
name="checkmark-circle-outline"
|
|
||||||
></ion-icon>
|
|
||||||
<span>Portfolio Performance</span>
|
|
||||||
</li>
|
|
||||||
<li class="align-items-center d-flex mb-1">
|
|
||||||
<ion-icon
|
|
||||||
class="mr-1 text-muted"
|
|
||||||
name="checkmark-circle-outline"
|
|
||||||
></ion-icon>
|
|
||||||
<span>Portfolio Summary</span>
|
|
||||||
</li>
|
|
||||||
<li class="align-items-center d-flex mb-1">
|
|
||||||
<ion-icon
|
|
||||||
class="mr-1 text-muted"
|
|
||||||
name="checkmark-circle-outline"
|
|
||||||
></ion-icon>
|
|
||||||
<span>Unlimited Transactions</span>
|
|
||||||
</li>
|
|
||||||
<li class="align-items-center d-flex mb-1">
|
|
||||||
<ion-icon
|
|
||||||
class="mr-1 text-muted"
|
|
||||||
name="checkmark-circle-outline"
|
|
||||||
></ion-icon>
|
|
||||||
<span>Advanced Insights</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p class="h5 text-right">
|
|
||||||
<span class="font-weight-normal"
|
|
||||||
>{{ user?.settings.baseCurrency || baseCurrency }}
|
|
||||||
<strong>2.99</strong>
|
|
||||||
<del class="ml-1 text-muted">3.99</del> / Month</span
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
</mat-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-5 row">
|
<div class="mb-5 row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h3 class="mb-3 text-center" i18n>Changelog</h3>
|
<h3 class="mb-3 text-center" i18n>Changelog</h3>
|
||||||
|
@ -7,10 +7,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mat-card {
|
.mat-card {
|
||||||
&.active {
|
|
||||||
border-color: rgba(var(--palette-primary-500), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.changelog {
|
&.changelog {
|
||||||
::ng-deep {
|
::ng-deep {
|
||||||
markdown {
|
markdown {
|
||||||
|
@ -8,11 +8,10 @@ import {
|
|||||||
hasPermission,
|
hasPermission,
|
||||||
permissions
|
permissions
|
||||||
} from '@ghostfolio/helper';
|
} from '@ghostfolio/helper';
|
||||||
|
import { Currency } from '@prisma/client';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
import { Currency } from '.prisma/client';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'gf-account-page',
|
selector: 'gf-account-page',
|
||||||
templateUrl: './account-page.html',
|
templateUrl: './account-page.html',
|
||||||
|
@ -55,11 +55,7 @@
|
|||||||
<div *ngIf="accesses?.length > 0" class="row">
|
<div *ngIf="accesses?.length > 0" class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h3 class="mb-3 text-center" i18n>Granted Access</h3>
|
<h3 class="mb-3 text-center" i18n>Granted Access</h3>
|
||||||
<mat-card class="mb-3">
|
<gf-access-table [accesses]="accesses"></gf-access-table>
|
||||||
<mat-card-content>
|
|
||||||
<gf-access-table [accesses]="accesses"></gf-access-table>
|
|
||||||
</mat-card-content>
|
|
||||||
</mat-card>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||||
|
|
||||||
|
import { AccountsPageComponent } from './accounts-page.component';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{ path: '', component: AccountsPageComponent, canActivate: [AuthGuard] }
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class AccountsPageRoutingModule {}
|
192
apps/client/src/app/pages/accounts/accounts-page.component.ts
Normal file
192
apps/client/src/app/pages/accounts/accounts-page.component.ts
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
|
||||||
|
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
|
||||||
|
import { User } from '@ghostfolio/api/app/user/interfaces/user.interface';
|
||||||
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
|
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||||
|
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||||
|
import { hasPermission, permissions } from '@ghostfolio/helper';
|
||||||
|
import { Account as AccountModel, AccountType } from '@prisma/client';
|
||||||
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
|
import { Subject, Subscription } from 'rxjs';
|
||||||
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog/create-or-update-account-dialog.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'gf-accounts-page',
|
||||||
|
templateUrl: './accounts-page.html',
|
||||||
|
styleUrls: ['./accounts-page.scss']
|
||||||
|
})
|
||||||
|
export class AccountsPageComponent implements OnInit {
|
||||||
|
public accounts: AccountModel[];
|
||||||
|
public deviceType: string;
|
||||||
|
public hasImpersonationId: boolean;
|
||||||
|
public hasPermissionToCreateAccount: boolean;
|
||||||
|
public hasPermissionToDeleteAccount: boolean;
|
||||||
|
public routeQueryParams: Subscription;
|
||||||
|
public user: User;
|
||||||
|
|
||||||
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
public constructor(
|
||||||
|
private cd: ChangeDetectorRef,
|
||||||
|
private dataService: DataService,
|
||||||
|
private deviceService: DeviceDetectorService,
|
||||||
|
private dialog: MatDialog,
|
||||||
|
private impersonationStorageService: ImpersonationStorageService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private router: Router,
|
||||||
|
private tokenStorageService: TokenStorageService
|
||||||
|
) {
|
||||||
|
this.routeQueryParams = route.queryParams
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((params) => {
|
||||||
|
if (params['createDialog']) {
|
||||||
|
this.openCreateAccountDialog();
|
||||||
|
} else if (params['editDialog']) {
|
||||||
|
if (this.accounts) {
|
||||||
|
const account = this.accounts.find((account) => {
|
||||||
|
return account.id === params['transactionId'];
|
||||||
|
});
|
||||||
|
|
||||||
|
this.openUpdateAccountDialog(account);
|
||||||
|
} else {
|
||||||
|
this.router.navigate(['.'], { relativeTo: this.route });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the controller
|
||||||
|
*/
|
||||||
|
public ngOnInit() {
|
||||||
|
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||||
|
|
||||||
|
this.impersonationStorageService
|
||||||
|
.onChangeHasImpersonation()
|
||||||
|
.subscribe((aId) => {
|
||||||
|
this.hasImpersonationId = !!aId;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.tokenStorageService
|
||||||
|
.onChangeHasToken()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.dataService.fetchUser().subscribe((user) => {
|
||||||
|
this.user = user;
|
||||||
|
this.hasPermissionToCreateAccount = hasPermission(
|
||||||
|
user.permissions,
|
||||||
|
permissions.createAccount
|
||||||
|
);
|
||||||
|
this.hasPermissionToDeleteAccount = hasPermission(
|
||||||
|
user.permissions,
|
||||||
|
permissions.deleteAccount
|
||||||
|
);
|
||||||
|
|
||||||
|
this.cd.markForCheck();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.fetchAccounts();
|
||||||
|
}
|
||||||
|
|
||||||
|
public fetchAccounts() {
|
||||||
|
this.dataService.fetchAccounts().subscribe((response) => {
|
||||||
|
this.accounts = response;
|
||||||
|
|
||||||
|
if (this.accounts?.length <= 0) {
|
||||||
|
this.router.navigate([], { queryParams: { createDialog: true } });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cd.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public onDeleteAccount(aId: string) {
|
||||||
|
this.dataService.deleteAccount(aId).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.fetchAccounts();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public onUpdateAccount(aAccount: AccountModel) {
|
||||||
|
this.router.navigate([], {
|
||||||
|
queryParams: { editDialog: true, transactionId: aAccount.id }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public openUpdateAccountDialog({
|
||||||
|
accountType,
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
platformId
|
||||||
|
}: AccountModel): void {
|
||||||
|
const dialogRef = this.dialog.open(CreateOrUpdateAccountDialog, {
|
||||||
|
data: {
|
||||||
|
account: {
|
||||||
|
accountType,
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
platformId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||||
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef.afterClosed().subscribe((data: any) => {
|
||||||
|
const account: UpdateAccountDto = data?.account;
|
||||||
|
|
||||||
|
if (account) {
|
||||||
|
this.dataService.putAccount(account).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.fetchAccounts();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.router.navigate(['.'], { relativeTo: this.route });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
private openCreateAccountDialog(): void {
|
||||||
|
const dialogRef = this.dialog.open(CreateOrUpdateAccountDialog, {
|
||||||
|
data: {
|
||||||
|
account: {
|
||||||
|
accountType: AccountType.SECURITIES,
|
||||||
|
name: null,
|
||||||
|
platformId: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||||
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef.afterClosed().subscribe((data: any) => {
|
||||||
|
const account: CreateAccountDto = data?.account;
|
||||||
|
|
||||||
|
if (account) {
|
||||||
|
this.dataService.postAccount(account).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.fetchAccounts();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.router.navigate(['.'], { relativeTo: this.route });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
31
apps/client/src/app/pages/accounts/accounts-page.html
Normal file
31
apps/client/src/app/pages/accounts/accounts-page.html
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col">
|
||||||
|
<h3 class="d-flex justify-content-center mb-3" i18n>Accounts</h3>
|
||||||
|
<gf-accounts-table
|
||||||
|
[accounts]="accounts"
|
||||||
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
|
[deviceType]="deviceType"
|
||||||
|
[locale]="user?.settings?.locale"
|
||||||
|
[showActions]="!hasImpersonationId && hasPermissionToDeleteAccount"
|
||||||
|
(accountDeleted)="onDeleteAccount($event)"
|
||||||
|
(accountToUpdate)="onUpdateAccount($event)"
|
||||||
|
></gf-accounts-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
*ngIf="!hasImpersonationId && hasPermissionToCreateAccount"
|
||||||
|
class="fab-container"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="align-items-center d-flex justify-content-center"
|
||||||
|
color="primary"
|
||||||
|
mat-fab
|
||||||
|
[routerLink]="[]"
|
||||||
|
[queryParams]="{ createDialog: true }"
|
||||||
|
>
|
||||||
|
<ion-icon name="add-outline" size="large"></ion-icon>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
25
apps/client/src/app/pages/accounts/accounts-page.module.ts
Normal file
25
apps/client/src/app/pages/accounts/accounts-page.module.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { GfAccountsTableModule } from '@ghostfolio/client/components/accounts-table/accounts-table.module';
|
||||||
|
|
||||||
|
import { AccountsPageRoutingModule } from './accounts-page-routing.module';
|
||||||
|
import { AccountsPageComponent } from './accounts-page.component';
|
||||||
|
import { CreateOrUpdateAccountDialogModule } from './create-or-update-account-dialog/create-or-update-account-dialog.module';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [AccountsPageComponent],
|
||||||
|
exports: [],
|
||||||
|
imports: [
|
||||||
|
AccountsPageRoutingModule,
|
||||||
|
CommonModule,
|
||||||
|
CreateOrUpdateAccountDialogModule,
|
||||||
|
GfAccountsTableModule,
|
||||||
|
MatButtonModule,
|
||||||
|
RouterModule
|
||||||
|
],
|
||||||
|
providers: [],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
})
|
||||||
|
export class AccountsPageModule {}
|
8
apps/client/src/app/pages/accounts/accounts-page.scss
Normal file
8
apps/client/src/app/pages/accounts/accounts-page.scss
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
:host {
|
||||||
|
.fab-container {
|
||||||
|
position: fixed;
|
||||||
|
right: 2rem;
|
||||||
|
bottom: 2rem;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
|
||||||
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
|
import { Currency } from '@prisma/client';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
|
import { DataService } from '../../../services/data.service';
|
||||||
|
import { CreateOrUpdateAccountDialogParams } from './interfaces/interfaces';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
host: { class: 'h-100' },
|
||||||
|
selector: 'gf-create-or-update-account-dialog',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
styleUrls: ['./create-or-update-account-dialog.scss'],
|
||||||
|
templateUrl: 'create-or-update-account-dialog.html'
|
||||||
|
})
|
||||||
|
export class CreateOrUpdateAccountDialog {
|
||||||
|
public currencies: Currency[] = [];
|
||||||
|
public platforms: { id: string; name: string }[];
|
||||||
|
|
||||||
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private dataService: DataService,
|
||||||
|
public dialogRef: MatDialogRef<CreateOrUpdateAccountDialog>,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateAccountDialogParams
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.dataService.fetchInfo().subscribe(({ currencies, platforms }) => {
|
||||||
|
this.currencies = currencies;
|
||||||
|
this.platforms = platforms;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public onCancel(): void {
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
<form #addAccountForm="ngForm" class="d-flex flex-column h-100">
|
||||||
|
<h1 *ngIf="data.account.id" mat-dialog-title i18n>Update account</h1>
|
||||||
|
<h1 *ngIf="!data.account.id" mat-dialog-title i18n>Add account</h1>
|
||||||
|
<div class="flex-grow-1" mat-dialog-content>
|
||||||
|
<div>
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label i18n>Name</mat-label>
|
||||||
|
<input matInput name="name" required [(ngModel)]="data.account.name" />
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div class="d-none">
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label i18n>Type</mat-label>
|
||||||
|
<mat-select name="type" required [(value)]="data.account.accountType">
|
||||||
|
<mat-option value="SECURITIES" i18n> SECURITIES </mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label i18n>Platform</mat-label>
|
||||||
|
<mat-select name="platformId" [(value)]="data.account.platformId">
|
||||||
|
<mat-option [value]="null"></mat-option>
|
||||||
|
<mat-option *ngFor="let platform of platforms" [value]="platform.id"
|
||||||
|
>{{ platform.name }}</mat-option
|
||||||
|
>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</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]="!addAccountForm.form.valid"
|
||||||
|
[mat-dialog-close]="data"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
@ -0,0 +1,27 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
|
|
||||||
|
import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [CreateOrUpdateAccountDialog],
|
||||||
|
exports: [],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatDialogModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatSelectModule,
|
||||||
|
ReactiveFormsModule
|
||||||
|
],
|
||||||
|
providers: []
|
||||||
|
})
|
||||||
|
export class CreateOrUpdateAccountDialogModule {}
|
@ -0,0 +1,43 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
.mat-dialog-content {
|
||||||
|
max-height: unset;
|
||||||
|
|
||||||
|
.autocomplete {
|
||||||
|
font-size: 90%;
|
||||||
|
height: 2.5rem;
|
||||||
|
|
||||||
|
.symbol {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-select {
|
||||||
|
&.no-arrow {
|
||||||
|
::ng-deep {
|
||||||
|
.mat-select-arrow {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-datepicker-input {
|
||||||
|
&.mat-input-element:disabled {
|
||||||
|
color: var(--dark-primary-text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(.is-dark-theme) {
|
||||||
|
.mat-dialog-content {
|
||||||
|
.mat-datepicker-input {
|
||||||
|
&.mat-input-element:disabled {
|
||||||
|
color: var(--light-primary-text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
import { Account } from '@prisma/client';
|
||||||
|
|
||||||
|
export interface CreateOrUpdateAccountDialogParams {
|
||||||
|
account: Account;
|
||||||
|
}
|
@ -1,8 +1,10 @@
|
|||||||
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||||
import { AdminData } from '@ghostfolio/api/app/admin/interfaces/admin-data.interface';
|
import { AdminData } from '@ghostfolio/api/app/admin/interfaces/admin-data.interface';
|
||||||
|
import { User } from '@ghostfolio/api/app/user/interfaces/user.interface';
|
||||||
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';
|
||||||
|
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||||
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/helper';
|
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/helper';
|
||||||
import { formatDistanceToNow, isValid, parseISO, sub } from 'date-fns';
|
import { formatDistanceToNow, isValid, parseISO, sub } from 'date-fns';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
@ -20,6 +22,7 @@ export class AdminPageComponent implements OnInit {
|
|||||||
public lastDataGathering: string;
|
public lastDataGathering: string;
|
||||||
public transactionCount: number;
|
public transactionCount: number;
|
||||||
public userCount: number;
|
public userCount: number;
|
||||||
|
public user: User;
|
||||||
public users: AdminData['users'];
|
public users: AdminData['users'];
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
@ -31,46 +34,24 @@ export class AdminPageComponent implements OnInit {
|
|||||||
private adminService: AdminService,
|
private adminService: AdminService,
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
private cd: ChangeDetectorRef,
|
private cd: ChangeDetectorRef,
|
||||||
private dataService: DataService
|
private dataService: DataService,
|
||||||
|
private tokenStorageService: TokenStorageService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the controller
|
* Initializes the controller
|
||||||
*/
|
*/
|
||||||
public ngOnInit() {
|
public ngOnInit() {
|
||||||
this.dataService
|
this.fetchAdminData();
|
||||||
.fetchAdminData()
|
|
||||||
|
this.tokenStorageService
|
||||||
|
.onChangeHasToken()
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(
|
.subscribe(() => {
|
||||||
({
|
this.dataService.fetchUser().subscribe((user) => {
|
||||||
exchangeRates,
|
this.user = user;
|
||||||
lastDataGathering,
|
});
|
||||||
transactionCount,
|
});
|
||||||
userCount,
|
|
||||||
users
|
|
||||||
}) => {
|
|
||||||
this.exchangeRates = exchangeRates;
|
|
||||||
this.users = users;
|
|
||||||
|
|
||||||
if (isValid(parseISO(lastDataGathering?.toString()))) {
|
|
||||||
this.lastDataGathering = formatDistanceToNow(
|
|
||||||
new Date(lastDataGathering),
|
|
||||||
{
|
|
||||||
addSuffix: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else if (lastDataGathering === 'IN_PROGRESS') {
|
|
||||||
this.dataGatheringInProgress = true;
|
|
||||||
} else {
|
|
||||||
this.lastDataGathering = '-';
|
|
||||||
}
|
|
||||||
|
|
||||||
this.transactionCount = transactionCount;
|
|
||||||
this.userCount = userCount;
|
|
||||||
|
|
||||||
this.cd.markForCheck();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public onFlushCache() {
|
public onFlushCache() {
|
||||||
@ -112,8 +93,56 @@ export class AdminPageComponent implements OnInit {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onDeleteUser(aId: string) {
|
||||||
|
const confirmation = confirm('Do you really want to delete this user?');
|
||||||
|
|
||||||
|
if (confirmation) {
|
||||||
|
this.dataService.deleteUser(aId).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.fetchAdminData();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
this.unsubscribeSubject.next();
|
this.unsubscribeSubject.next();
|
||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fetchAdminData() {
|
||||||
|
this.dataService
|
||||||
|
.fetchAdminData()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(
|
||||||
|
({
|
||||||
|
exchangeRates,
|
||||||
|
lastDataGathering,
|
||||||
|
transactionCount,
|
||||||
|
userCount,
|
||||||
|
users
|
||||||
|
}) => {
|
||||||
|
this.exchangeRates = exchangeRates;
|
||||||
|
this.users = users;
|
||||||
|
|
||||||
|
if (isValid(parseISO(lastDataGathering?.toString()))) {
|
||||||
|
this.lastDataGathering = formatDistanceToNow(
|
||||||
|
new Date(lastDataGathering),
|
||||||
|
{
|
||||||
|
addSuffix: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else if (lastDataGathering === 'IN_PROGRESS') {
|
||||||
|
this.dataGatheringInProgress = true;
|
||||||
|
} else {
|
||||||
|
this.lastDataGathering = '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.transactionCount = transactionCount;
|
||||||
|
this.userCount = userCount;
|
||||||
|
|
||||||
|
this.cd.markForCheck();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -69,44 +69,59 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h3 class="mb-3 text-center" i18n>Users</h3>
|
<h3 class="mb-3 text-center" i18n>Users</h3>
|
||||||
<mat-card class="px-0">
|
<div class="users">
|
||||||
<mat-card-content class="users">
|
<table class="gf-table">
|
||||||
<table>
|
<thead>
|
||||||
<thead>
|
<tr class="mat-header-row">
|
||||||
<tr class="mat-header-row">
|
<th class="mat-header-cell px-1 py-2" i18n>User</th>
|
||||||
<th class="mat-header-cell pl-3 py-2" i18n>User</th>
|
<th class="mat-header-cell px-1 py-2" i18n>Registration Date</th>
|
||||||
<th class="mat-header-cell pr-2 py-2" i18n>
|
<th class="mat-header-cell px-1 py-2" i18n>Accounts</th>
|
||||||
Registration Date
|
<th class="mat-header-cell px-1 py-2" i18n>Transactions</th>
|
||||||
</th>
|
<th class="mat-header-cell px-1 py-2" i18n>Engagement</th>
|
||||||
<th class="mat-header-cell pr-2 py-2" i18n>Accounts</th>
|
<th class="mat-header-cell px-1 py-2" i18n>Last Activitiy</th>
|
||||||
<th class="mat-header-cell pr-2 py-2" i18n>Transactions</th>
|
<th class="mat-header-cell px-1 py-2"></th>
|
||||||
<th class="mat-header-cell pr-2 py-2" i18n>Engagement</th>
|
</tr>
|
||||||
<th class="mat-header-cell pr-3 py-2" i18n>Last Activitiy</th>
|
</thead>
|
||||||
</tr>
|
<tbody>
|
||||||
</thead>
|
<tr *ngFor="let userItem of users" class="mat-row">
|
||||||
<tbody>
|
<td class="mat-cell px-1 py-2">
|
||||||
<tr *ngFor="let userItem of users" class="mat-row">
|
{{ userItem.alias || userItem.id }}
|
||||||
<td class="mat-cell pl-3 py-2">
|
</td>
|
||||||
{{ userItem.alias || userItem.id }}
|
<td class="mat-cell px-1 py-2">
|
||||||
</td>
|
{{ userItem.createdAt | date: defaultDateFormat }}
|
||||||
<td class="mat-cell pr-2 py-2">
|
</td>
|
||||||
{{ userItem.createdAt | date: defaultDateFormat }}
|
<td class="mat-cell px-1 py-2">{{ userItem._count?.Account }}</td>
|
||||||
</td>
|
<td class="mat-cell px-1 py-2">{{ userItem._count?.Order }}</td>
|
||||||
<td class="mat-cell pr-2 py-2">
|
<td class="mat-cell px-1 py-2">
|
||||||
{{ userItem._count?.Account }}
|
{{ userItem.Analytics?.activityCount }}
|
||||||
</td>
|
</td>
|
||||||
<td class="mat-cell pr-2 py-2">{{ userItem._count?.Order }}</td>
|
<td class="mat-cell px-1 py-2">
|
||||||
<td class="mat-cell pr-2 py-2">
|
{{ formatDistanceToNow(userItem.Analytics?.updatedAt) }}
|
||||||
{{ userItem.Analytics?.activityCount }}
|
</td>
|
||||||
</td>
|
<td class="mat-cell px-1 py-2">
|
||||||
<td class="mat-cell pr-3 py-2">
|
<button
|
||||||
{{ formatDistanceToNow(userItem.Analytics?.updatedAt) }}
|
class="mx-1 no-min-width px-2"
|
||||||
</td>
|
mat-button
|
||||||
</tr>
|
[matMenuTriggerFor]="accountMenu"
|
||||||
</tbody>
|
(click)="$event.stopPropagation()"
|
||||||
</table>
|
>
|
||||||
</mat-card-content>
|
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||||
</mat-card>
|
</button>
|
||||||
|
<mat-menu #accountMenu="matMenu" xPosition="before">
|
||||||
|
<button
|
||||||
|
i18n
|
||||||
|
mat-menu-item
|
||||||
|
[disabled]="userItem.id === user?.id"
|
||||||
|
(click)="onDeleteUser(userItem.id)"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</mat-menu>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
import { CacheService } from '@ghostfolio/client/services/cache.service';
|
import { CacheService } from '@ghostfolio/client/services/cache.service';
|
||||||
|
|
||||||
import { AdminPageRoutingModule } from './admin-page-routing.module';
|
import { AdminPageRoutingModule } from './admin-page-routing.module';
|
||||||
@ -14,7 +15,8 @@ import { AdminPageComponent } from './admin-page.component';
|
|||||||
AdminPageRoutingModule,
|
AdminPageRoutingModule,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatCardModule
|
MatCardModule,
|
||||||
|
MatMenuModule
|
||||||
],
|
],
|
||||||
providers: [CacheService],
|
providers: [CacheService],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
@ -1,27 +1,18 @@
|
|||||||
|
@import '~apps/client/src/styles/ghostfolio-style';
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
color: rgb(var(--dark-primary-text));
|
color: rgb(var(--dark-primary-text));
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
.mat-card-content {
|
.users {
|
||||||
&.users {
|
overflow-x: auto;
|
||||||
overflow-x: auto;
|
|
||||||
|
|
||||||
table {
|
table {
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
|
|
||||||
tr {
|
.mat-row,
|
||||||
&:nth-child(even) {
|
.mat-header-row {
|
||||||
background-color: rgba(
|
width: 100%;
|
||||||
var(--dark-primary-text),
|
|
||||||
var(--palette-background-hover-alpha)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mat-row,
|
|
||||||
.mat-header-row {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -41,15 +32,4 @@
|
|||||||
|
|
||||||
:host-context(.is-dark-theme) {
|
:host-context(.is-dark-theme) {
|
||||||
color: rgb(var(--light-primary-text));
|
color: rgb(var(--light-primary-text));
|
||||||
|
|
||||||
.mat-table {
|
|
||||||
tr {
|
|
||||||
&:nth-child(even) {
|
|
||||||
background-color: rgba(
|
|
||||||
var(--light-primary-text),
|
|
||||||
var(--palette-background-hover-alpha)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,9 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
styleUrls: ['./analysis-page.scss']
|
styleUrls: ['./analysis-page.scss']
|
||||||
})
|
})
|
||||||
export class AnalysisPageComponent implements OnDestroy, OnInit {
|
export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||||
|
public accounts: {
|
||||||
|
[symbol: string]: Pick<PortfolioPosition, 'name'> & { value: number };
|
||||||
|
};
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
public period = 'current';
|
public period = 'current';
|
||||||
public periodOptions: ToggleOption[] = [
|
public periodOptions: ToggleOption[] = [
|
||||||
@ -23,9 +26,6 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
{ label: 'Current', value: 'current' }
|
{ label: 'Current', value: 'current' }
|
||||||
];
|
];
|
||||||
public hasImpersonationId: boolean;
|
public hasImpersonationId: boolean;
|
||||||
public platforms: {
|
|
||||||
[symbol: string]: Pick<PortfolioPosition, 'name'> & { value: number };
|
|
||||||
};
|
|
||||||
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 };
|
||||||
@ -95,7 +95,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
},
|
},
|
||||||
aPeriod: string
|
aPeriod: string
|
||||||
) {
|
) {
|
||||||
this.platforms = {};
|
this.accounts = {};
|
||||||
this.positions = {};
|
this.positions = {};
|
||||||
this.positionsArray = [];
|
this.positionsArray = [];
|
||||||
|
|
||||||
@ -108,21 +108,21 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
type: position.type,
|
type: position.type,
|
||||||
value:
|
value:
|
||||||
aPeriod === 'original'
|
aPeriod === 'original'
|
||||||
? position.shareInvestment
|
? position.allocationInvestment
|
||||||
: position.shareCurrent
|
: position.allocationCurrent
|
||||||
};
|
};
|
||||||
this.positionsArray.push(position);
|
this.positionsArray.push(position);
|
||||||
|
|
||||||
for (const [platform, { current, original }] of Object.entries(
|
for (const [account, { current, original }] of Object.entries(
|
||||||
position.platforms
|
position.accounts
|
||||||
)) {
|
)) {
|
||||||
if (this.platforms[platform]?.value) {
|
if (this.accounts[account]?.value) {
|
||||||
this.platforms[platform].value +=
|
this.accounts[account].value +=
|
||||||
aPeriod === 'original' ? original : current;
|
aPeriod === 'original' ? original : current;
|
||||||
} else {
|
} else {
|
||||||
this.platforms[platform] = {
|
this.accounts[account] = {
|
||||||
value: aPeriod === 'original' ? original : current,
|
value: aPeriod === 'original' ? original : current,
|
||||||
name: platform
|
name: account
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,15 @@
|
|||||||
<div class="mb-5 row">
|
<div class="mb-5 row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h3 class="d-flex justify-content-center mb-3" i18n>Analysis</h3>
|
<h3 class="d-flex justify-content-center mb-3" i18n>Analysis</h3>
|
||||||
<gf-positions-table
|
<div class="mb-4">
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
<h4 class="m-0" i18n>Positions</h4>
|
||||||
[deviceType]="deviceType"
|
<gf-positions-table
|
||||||
[locale]="user?.settings?.locale"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
[positions]="positionsArray"
|
[deviceType]="deviceType"
|
||||||
></gf-positions-table>
|
[locale]="user?.settings?.locale"
|
||||||
|
[positions]="positionsArray"
|
||||||
|
></gf-positions-table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="proportion-charts row">
|
<div class="proportion-charts row">
|
||||||
@ -36,7 +39,7 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
<mat-card-header class="w-100">
|
<mat-card-header class="w-100">
|
||||||
<mat-card-title i18n>By Platform</mat-card-title>
|
<mat-card-title i18n>By Account</mat-card-title>
|
||||||
<gf-toggle
|
<gf-toggle
|
||||||
[defaultValue]="period"
|
[defaultValue]="period"
|
||||||
[isLoading]="false"
|
[isLoading]="false"
|
||||||
@ -50,7 +53,7 @@
|
|||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
[isInPercent]="hasImpersonationId"
|
[isInPercent]="hasImpersonationId"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[positions]="platforms"
|
[positions]="accounts"
|
||||||
></gf-portfolio-proportion-chart>
|
></gf-portfolio-proportion-chart>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
@ -157,7 +160,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div i18n>
|
<div i18n>
|
||||||
You can find more charts on your desktop:
|
You can find more charts on your desktop:
|
||||||
<a href="https://ghostfol.io" target="_blank">www.ghostfol.io</a>
|
<a href="https://ghostfol.io" target="_blank">Ghostfol.io</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
|
@ -156,4 +156,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="downloads my-5 row justify-content-center">
|
||||||
|
<a
|
||||||
|
href="https://play.google.com/store/apps/details?id=ch.dotsilver.ghostfolio.twa"
|
||||||
|
title="Get Ghostfolio on Google Play"
|
||||||
|
>
|
||||||
|
<img alt="Google Play Badge" src="assets/badge-en-google-play.png" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -18,6 +18,12 @@
|
|||||||
background-color: var(--light-background);
|
background-color: var(--light-background);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.downloads {
|
||||||
|
img {
|
||||||
|
height: 2.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:host-context(.is-dark-theme) {
|
:host-context(.is-dark-theme) {
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
|
||||||
|
import { PricingPageComponent } from './pricing-page.component';
|
||||||
|
|
||||||
|
const routes: Routes = [{ path: '', component: PricingPageComponent }];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class PricingPageRoutingModule {}
|
53
apps/client/src/app/pages/pricing/pricing-page.component.ts
Normal file
53
apps/client/src/app/pages/pricing/pricing-page.component.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||||
|
import { User } from '@ghostfolio/api/app/user/interfaces/user.interface';
|
||||||
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
|
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||||
|
import { baseCurrency } from '@ghostfolio/helper';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'gf-pricing-page',
|
||||||
|
templateUrl: './pricing-page.html',
|
||||||
|
styleUrls: ['./pricing-page.scss']
|
||||||
|
})
|
||||||
|
export class PricingPageComponent implements OnInit {
|
||||||
|
public baseCurrency = baseCurrency;
|
||||||
|
public isLoggedIn: boolean;
|
||||||
|
public user: User;
|
||||||
|
|
||||||
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
public constructor(
|
||||||
|
private cd: ChangeDetectorRef,
|
||||||
|
private dataService: DataService,
|
||||||
|
private tokenStorageService: TokenStorageService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the controller
|
||||||
|
*/
|
||||||
|
public ngOnInit() {
|
||||||
|
this.isLoggedIn = !!this.tokenStorageService.getToken();
|
||||||
|
|
||||||
|
if (this.isLoggedIn)
|
||||||
|
this.tokenStorageService
|
||||||
|
.onChangeHasToken()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.dataService.fetchUser().subscribe((user) => {
|
||||||
|
this.user = user;
|
||||||
|
|
||||||
|
this.cd.markForCheck();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
|
}
|
||||||
|
}
|
102
apps/client/src/app/pages/pricing/pricing-page.html
Normal file
102
apps/client/src/app/pages/pricing/pricing-page.html
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<h3 class="d-flex justify-content-center mb-3" i18n>Pricing Plans</h3>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-12 col-md-6">
|
||||||
|
<mat-card class="mb-3">
|
||||||
|
<h4 i18n>Open Source</h4>
|
||||||
|
<p>Host your <strong>Ghostfolio</strong> instance by yourself.</p>
|
||||||
|
<ul class="list-unstyled mb-3">
|
||||||
|
<li class="align-items-center d-flex mb-1">
|
||||||
|
<ion-icon
|
||||||
|
class="mr-1 text-muted"
|
||||||
|
name="checkmark-circle-outline"
|
||||||
|
></ion-icon>
|
||||||
|
<span>Portfolio Performance</span>
|
||||||
|
</li>
|
||||||
|
<li class="align-items-center d-flex mb-1">
|
||||||
|
<ion-icon
|
||||||
|
class="mr-1 text-muted"
|
||||||
|
name="checkmark-circle-outline"
|
||||||
|
></ion-icon>
|
||||||
|
<span>Portfolio Summary</span>
|
||||||
|
</li>
|
||||||
|
<li class="align-items-center d-flex mb-1">
|
||||||
|
<ion-icon
|
||||||
|
class="mr-1 text-muted"
|
||||||
|
name="checkmark-circle-outline"
|
||||||
|
></ion-icon>
|
||||||
|
<span>Unlimited Transactions</span>
|
||||||
|
</li>
|
||||||
|
<li class="align-items-center d-flex mb-1">
|
||||||
|
<ion-icon
|
||||||
|
class="mr-1 text-muted"
|
||||||
|
name="checkmark-circle-outline"
|
||||||
|
></ion-icon>
|
||||||
|
<span>Advanced Insights</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p class="h5 text-right">
|
||||||
|
<span>Free</span>
|
||||||
|
</p>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 col-md-6">
|
||||||
|
<mat-card
|
||||||
|
class="mb-3"
|
||||||
|
[ngClass]="{ 'active': user?.subscription?.type === 'Trial' }"
|
||||||
|
>
|
||||||
|
<h4 class="align-items-center d-flex" i18n>
|
||||||
|
Diamond
|
||||||
|
<ion-icon
|
||||||
|
class="ml-1 text-muted"
|
||||||
|
name="diamond-outline"
|
||||||
|
></ion-icon>
|
||||||
|
</h4>
|
||||||
|
<p>
|
||||||
|
Get a fully managed <strong>Ghostfolio</strong> cloud offering.
|
||||||
|
</p>
|
||||||
|
<ul class="list-unstyled mb-3">
|
||||||
|
<li class="align-items-center d-flex mb-1">
|
||||||
|
<ion-icon
|
||||||
|
class="mr-1 text-muted"
|
||||||
|
name="checkmark-circle-outline"
|
||||||
|
></ion-icon>
|
||||||
|
<span>Portfolio Performance</span>
|
||||||
|
</li>
|
||||||
|
<li class="align-items-center d-flex mb-1">
|
||||||
|
<ion-icon
|
||||||
|
class="mr-1 text-muted"
|
||||||
|
name="checkmark-circle-outline"
|
||||||
|
></ion-icon>
|
||||||
|
<span>Portfolio Summary</span>
|
||||||
|
</li>
|
||||||
|
<li class="align-items-center d-flex mb-1">
|
||||||
|
<ion-icon
|
||||||
|
class="mr-1 text-muted"
|
||||||
|
name="checkmark-circle-outline"
|
||||||
|
></ion-icon>
|
||||||
|
<span>Unlimited Transactions</span>
|
||||||
|
</li>
|
||||||
|
<li class="align-items-center d-flex mb-1">
|
||||||
|
<ion-icon
|
||||||
|
class="mr-1 text-muted"
|
||||||
|
name="checkmark-circle-outline"
|
||||||
|
></ion-icon>
|
||||||
|
<span>Advanced Insights</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p class="h5 text-right">
|
||||||
|
<span class="font-weight-normal"
|
||||||
|
>{{ user?.settings.baseCurrency || baseCurrency }}
|
||||||
|
<strong>2.99</strong>
|
||||||
|
<del class="ml-1 text-muted">3.99</del> / Month</span
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
15
apps/client/src/app/pages/pricing/pricing-page.module.ts
Normal file
15
apps/client/src/app/pages/pricing/pricing-page.module.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
|
||||||
|
import { PricingPageRoutingModule } from './pricing-page-routing.module';
|
||||||
|
import { PricingPageComponent } from './pricing-page.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [PricingPageComponent],
|
||||||
|
exports: [],
|
||||||
|
imports: [CommonModule, MatCardModule, PricingPageRoutingModule],
|
||||||
|
providers: [],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
})
|
||||||
|
export class PricingPageModule {}
|
14
apps/client/src/app/pages/pricing/pricing-page.scss
Normal file
14
apps/client/src/app/pages/pricing/pricing-page.scss
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
:host {
|
||||||
|
color: rgb(var(--dark-primary-text));
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
.mat-card {
|
||||||
|
&.active {
|
||||||
|
border-color: rgba(var(--palette-primary-500), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(.is-dark-theme) {
|
||||||
|
color: rgb(var(--light-primary-text));
|
||||||
|
}
|
@ -10,9 +10,9 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
styleUrls: ['./report-page.scss']
|
styleUrls: ['./report-page.scss']
|
||||||
})
|
})
|
||||||
export class ReportPageComponent implements OnInit {
|
export class ReportPageComponent implements OnInit {
|
||||||
|
public accountClusterRiskRules: PortfolioReportRule[];
|
||||||
public currencyClusterRiskRules: PortfolioReportRule[];
|
public currencyClusterRiskRules: PortfolioReportRule[];
|
||||||
public feeRules: PortfolioReportRule[];
|
public feeRules: PortfolioReportRule[];
|
||||||
public platformClusterRiskRules: PortfolioReportRule[];
|
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
@ -32,11 +32,11 @@ export class ReportPageComponent implements OnInit {
|
|||||||
.fetchPortfolioReport()
|
.fetchPortfolioReport()
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((portfolioReport) => {
|
.subscribe((portfolioReport) => {
|
||||||
|
this.accountClusterRiskRules =
|
||||||
|
portfolioReport.rules['accountClusterRisk'] || null;
|
||||||
this.currencyClusterRiskRules =
|
this.currencyClusterRiskRules =
|
||||||
portfolioReport.rules['currencyClusterRisk'] || null;
|
portfolioReport.rules['currencyClusterRisk'] || null;
|
||||||
this.feeRules = portfolioReport.rules['fees'] || null;
|
this.feeRules = portfolioReport.rules['fees'] || null;
|
||||||
this.platformClusterRiskRules =
|
|
||||||
portfolioReport.rules['platformClusterRisk'] || null;
|
|
||||||
|
|
||||||
this.cd.markForCheck();
|
this.cd.markForCheck();
|
||||||
});
|
});
|
||||||
|
@ -18,8 +18,8 @@
|
|||||||
<gf-rules [rules]="currencyClusterRiskRules"></gf-rules>
|
<gf-rules [rules]="currencyClusterRiskRules"></gf-rules>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h4 class="m-0" i18n>Platform Cluster Risks</h4>
|
<h4 class="m-0" i18n>Account Cluster Risks</h4>
|
||||||
<gf-rules [rules]="platformClusterRiskRules"></gf-rules>
|
<gf-rules [rules]="accountClusterRiskRules"></gf-rules>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 class="m-0" i18n>Fees</h4>
|
<h4 class="m-0" i18n>Fees</h4>
|
||||||
|
@ -30,6 +30,7 @@ import { CreateOrUpdateTransactionDialogParams } from './interfaces/interfaces';
|
|||||||
})
|
})
|
||||||
export class CreateOrUpdateTransactionDialog {
|
export class CreateOrUpdateTransactionDialog {
|
||||||
public currencies: Currency[] = [];
|
public currencies: Currency[] = [];
|
||||||
|
public currentMarketPrice = null;
|
||||||
public filteredLookupItems: Observable<LookupItem[]>;
|
public filteredLookupItems: Observable<LookupItem[]>;
|
||||||
public isLoading = false;
|
public isLoading = false;
|
||||||
public platforms: { id: string; name: string }[];
|
public platforms: { id: string; name: string }[];
|
||||||
@ -65,6 +66,20 @@ export class CreateOrUpdateTransactionDialog {
|
|||||||
return [];
|
return [];
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (this.data.transaction.symbol) {
|
||||||
|
this.dataService
|
||||||
|
.fetchSymbolItem(this.data.transaction.symbol)
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(({ marketPrice }) => {
|
||||||
|
this.currentMarketPrice = marketPrice;
|
||||||
|
this.cd.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public applyCurrentMarketPrice() {
|
||||||
|
this.data.transaction.unitPrice = this.currentMarketPrice;
|
||||||
}
|
}
|
||||||
|
|
||||||
public onCancel(): void {
|
public onCancel(): void {
|
||||||
@ -81,7 +96,7 @@ export class CreateOrUpdateTransactionDialog {
|
|||||||
.subscribe(({ currency, dataSource, marketPrice }) => {
|
.subscribe(({ currency, dataSource, marketPrice }) => {
|
||||||
this.data.transaction.currency = currency;
|
this.data.transaction.currency = currency;
|
||||||
this.data.transaction.dataSource = dataSource;
|
this.data.transaction.dataSource = dataSource;
|
||||||
this.data.transaction.unitPrice = marketPrice;
|
this.currentMarketPrice = marketPrice;
|
||||||
|
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
|
|
||||||
|
@ -81,7 +81,13 @@
|
|||||||
[matDatepicker]="date"
|
[matDatepicker]="date"
|
||||||
[(ngModel)]="data.transaction.date"
|
[(ngModel)]="data.transaction.date"
|
||||||
/>
|
/>
|
||||||
<mat-datepicker-toggle matSuffix [for]="date"></mat-datepicker-toggle>
|
<mat-datepicker-toggle matSuffix [for]="date">
|
||||||
|
<ion-icon
|
||||||
|
class="text-muted"
|
||||||
|
matDatepickerToggleIcon
|
||||||
|
name="calendar-clear-outline"
|
||||||
|
></ion-icon>
|
||||||
|
</mat-datepicker-toggle>
|
||||||
<mat-datepicker #date disabled="false"></mat-datepicker>
|
<mat-datepicker #date disabled="false"></mat-datepicker>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
@ -110,7 +116,7 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="unit-price w-100">
|
||||||
<mat-label i18n>Unit Price</mat-label>
|
<mat-label i18n>Unit Price</mat-label>
|
||||||
<input
|
<input
|
||||||
matInput
|
matInput
|
||||||
@ -119,13 +125,21 @@
|
|||||||
type="number"
|
type="number"
|
||||||
[(ngModel)]="data.transaction.unitPrice"
|
[(ngModel)]="data.transaction.unitPrice"
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
*ngIf="currentMarketPrice"
|
||||||
|
mat-icon-button
|
||||||
|
matSuffix
|
||||||
|
title="Apply current market price"
|
||||||
|
(click)="applyCurrentMarketPrice()"
|
||||||
|
>
|
||||||
|
<ion-icon class="text-muted" name="refresh-outline"></ion-icon>
|
||||||
|
</button>
|
||||||
</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>Account</mat-label>
|
<mat-label i18n>Account</mat-label>
|
||||||
<mat-select
|
<mat-select
|
||||||
disabled
|
|
||||||
name="accountId"
|
name="accountId"
|
||||||
required
|
required
|
||||||
[(value)]="data.transaction.accountId"
|
[(value)]="data.transaction.accountId"
|
||||||
@ -136,17 +150,6 @@
|
|||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
|
||||||
<mat-label i18n>Platform</mat-label>
|
|
||||||
<mat-select name="platformId" [(value)]="data.transaction.platformId">
|
|
||||||
<mat-option [value]="null"></mat-option>
|
|
||||||
<mat-option *ngFor="let platform of platforms" [value]="platform.id"
|
|
||||||
>{{ platform.name }}</mat-option
|
|
||||||
>
|
|
||||||
</mat-select>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="justify-content-end" mat-dialog-actions>
|
<div class="justify-content-end" mat-dialog-actions>
|
||||||
<button i18n mat-button (click)="onCancel()">Cancel</button>
|
<button i18n mat-button (click)="onCancel()">Cancel</button>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
@ -30,6 +30,7 @@ import { CreateOrUpdateTransactionDialog } from './create-or-update-transaction-
|
|||||||
MatSelectModule,
|
MatSelectModule,
|
||||||
ReactiveFormsModule
|
ReactiveFormsModule
|
||||||
],
|
],
|
||||||
providers: []
|
providers: [],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
export class CreateOrUpdateTransactionDialogModule {}
|
export class CreateOrUpdateTransactionDialogModule {}
|
||||||
|
@ -14,6 +14,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mat-form-field-appearance-outline {
|
||||||
|
::ng-deep {
|
||||||
|
.mat-form-field-suffix {
|
||||||
|
top: -0.3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-icon {
|
||||||
|
font-size: 130%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mat-select {
|
.mat-select {
|
||||||
&.no-arrow {
|
&.no-arrow {
|
||||||
::ng-deep {
|
::ng-deep {
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import { Account } from '@prisma/client';
|
import { Account, Order } from '@prisma/client';
|
||||||
|
|
||||||
import { Order } from '../../interfaces/order.interface';
|
|
||||||
|
|
||||||
export interface CreateOrUpdateTransactionDialogParams {
|
export interface CreateOrUpdateTransactionDialogParams {
|
||||||
accountId: string;
|
accountId: string;
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
import { Currency, DataSource } from '.prisma/client';
|
|
||||||
|
|
||||||
export interface Order {
|
|
||||||
accountId: string;
|
|
||||||
currency: Currency;
|
|
||||||
dataSource: DataSource;
|
|
||||||
date: Date;
|
|
||||||
fee: number;
|
|
||||||
id: string;
|
|
||||||
quantity: number;
|
|
||||||
platformId: string;
|
|
||||||
symbol: string;
|
|
||||||
type: string;
|
|
||||||
unitPrice: number;
|
|
||||||
}
|
|
@ -1,6 +1,7 @@
|
|||||||
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, 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 { 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 { User } from '@ghostfolio/api/app/user/interfaces/user.interface';
|
import { User } from '@ghostfolio/api/app/user/interfaces/user.interface';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
@ -108,6 +109,10 @@ export class TransactionsPageComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onCloneTransaction(aTransaction: OrderModel) {
|
||||||
|
this.openCreateTransactionDialog(aTransaction);
|
||||||
|
}
|
||||||
|
|
||||||
public onDeleteTransaction(aId: string) {
|
public onDeleteTransaction(aId: string) {
|
||||||
this.dataService.deleteOrder(aId).subscribe({
|
this.dataService.deleteOrder(aId).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
@ -129,7 +134,6 @@ export class TransactionsPageComponent implements OnInit {
|
|||||||
date,
|
date,
|
||||||
fee,
|
fee,
|
||||||
id,
|
id,
|
||||||
platformId,
|
|
||||||
quantity,
|
quantity,
|
||||||
symbol,
|
symbol,
|
||||||
type,
|
type,
|
||||||
@ -145,7 +149,6 @@ export class TransactionsPageComponent implements OnInit {
|
|||||||
date,
|
date,
|
||||||
fee,
|
fee,
|
||||||
id,
|
id,
|
||||||
platformId,
|
|
||||||
quantity,
|
quantity,
|
||||||
symbol,
|
symbol,
|
||||||
type,
|
type,
|
||||||
@ -176,21 +179,23 @@ export class TransactionsPageComponent implements OnInit {
|
|||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
private openCreateTransactionDialog(): void {
|
private openCreateTransactionDialog(aTransaction?: OrderModel): void {
|
||||||
const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, {
|
const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, {
|
||||||
data: {
|
data: {
|
||||||
accounts: this.user?.accounts,
|
accounts: this.user?.accounts,
|
||||||
transaction: {
|
transaction: {
|
||||||
accountId: this.user?.accounts.find((account) => {
|
accountId:
|
||||||
return account.isDefault;
|
aTransaction?.accountId ??
|
||||||
})?.id,
|
this.user?.accounts.find((account) => {
|
||||||
currency: null,
|
return account.isDefault;
|
||||||
|
})?.id,
|
||||||
|
currency: aTransaction?.currency ?? null,
|
||||||
|
dataSource: aTransaction?.dataSource ?? null,
|
||||||
date: new Date(),
|
date: new Date(),
|
||||||
fee: 0,
|
fee: 0,
|
||||||
platformId: null,
|
|
||||||
quantity: null,
|
quantity: null,
|
||||||
symbol: null,
|
symbol: aTransaction?.symbol ?? null,
|
||||||
type: 'BUY',
|
type: aTransaction?.type ?? 'BUY',
|
||||||
unitPrice: null
|
unitPrice: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -199,7 +204,7 @@ export class TransactionsPageComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe((data: any) => {
|
dialogRef.afterClosed().subscribe((data: any) => {
|
||||||
const transaction: UpdateOrderDto = data?.transaction;
|
const transaction: CreateOrderDto = data?.transaction;
|
||||||
|
|
||||||
if (transaction) {
|
if (transaction) {
|
||||||
this.dataService.postOrder(transaction).subscribe({
|
this.dataService.postOrder(transaction).subscribe({
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
[showActions]="!hasImpersonationId && hasPermissionToDeleteOrder"
|
[showActions]="!hasImpersonationId && hasPermissionToDeleteOrder"
|
||||||
[transactions]="transactions"
|
[transactions]="transactions"
|
||||||
(transactionDeleted)="onDeleteTransaction($event)"
|
(transactionDeleted)="onDeleteTransaction($event)"
|
||||||
|
(transactionToClone)="onCloneTransaction($event)"
|
||||||
(transactionToUpdate)="onUpdateTransaction($event)"
|
(transactionToUpdate)="onUpdateTransaction($event)"
|
||||||
></gf-transactions-table>
|
></gf-transactions-table>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Access } from '@ghostfolio/api/app/access/interfaces/access.interface';
|
import { Access } from '@ghostfolio/api/app/access/interfaces/access.interface';
|
||||||
|
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
|
||||||
|
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
|
||||||
import { AdminData } from '@ghostfolio/api/app/admin/interfaces/admin-data.interface';
|
import { AdminData } from '@ghostfolio/api/app/admin/interfaces/admin-data.interface';
|
||||||
import { InfoItem } from '@ghostfolio/api/app/info/interfaces/info-item.interface';
|
import { InfoItem } from '@ghostfolio/api/app/info/interfaces/info-item.interface';
|
||||||
|
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 { PortfolioItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-item.interface';
|
import { PortfolioItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-item.interface';
|
||||||
import { PortfolioOverview } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-overview.interface';
|
import { PortfolioOverview } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-overview.interface';
|
||||||
@ -19,6 +22,7 @@ import { UserItem } from '@ghostfolio/api/app/user/interfaces/user-item.interfac
|
|||||||
import { User } from '@ghostfolio/api/app/user/interfaces/user.interface';
|
import { User } from '@ghostfolio/api/app/user/interfaces/user.interface';
|
||||||
import { UpdateUserSettingsDto } from '@ghostfolio/api/app/user/update-user-settings.dto';
|
import { UpdateUserSettingsDto } from '@ghostfolio/api/app/user/update-user-settings.dto';
|
||||||
import { Order as OrderModel } from '@prisma/client';
|
import { Order as OrderModel } from '@prisma/client';
|
||||||
|
import { Account as AccountModel } from '@prisma/client';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@ -28,14 +32,26 @@ export class DataService {
|
|||||||
|
|
||||||
public constructor(private http: HttpClient) {}
|
public constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
public fetchAccounts() {
|
||||||
|
return this.http.get<AccountModel[]>('/api/account');
|
||||||
|
}
|
||||||
|
|
||||||
public fetchAdminData() {
|
public fetchAdminData() {
|
||||||
return this.http.get<AdminData>('/api/admin');
|
return this.http.get<AdminData>('/api/admin');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public deleteAccount(aId: string) {
|
||||||
|
return this.http.delete<any>(`/api/account/${aId}`);
|
||||||
|
}
|
||||||
|
|
||||||
public deleteOrder(aId: string) {
|
public deleteOrder(aId: string) {
|
||||||
return this.http.delete<any>(`/api/order/${aId}`);
|
return this.http.delete<any>(`/api/order/${aId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public deleteUser(aId: string) {
|
||||||
|
return this.http.delete<any>(`/api/user/${aId}`);
|
||||||
|
}
|
||||||
|
|
||||||
public fetchAccesses() {
|
public fetchAccesses() {
|
||||||
return this.http.get<Access[]>('/api/access');
|
return this.http.get<Access[]>('/api/access');
|
||||||
}
|
}
|
||||||
@ -108,7 +124,11 @@ export class DataService {
|
|||||||
return this.http.get<any>(`/api/auth/anonymous/${accessToken}`);
|
return this.http.get<any>(`/api/auth/anonymous/${accessToken}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public postOrder(aOrder: UpdateOrderDto) {
|
public postAccount(aAccount: CreateAccountDto) {
|
||||||
|
return this.http.post<OrderModel>(`/api/account`, aAccount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public postOrder(aOrder: CreateOrderDto) {
|
||||||
return this.http.post<OrderModel>(`/api/order`, aOrder);
|
return this.http.post<OrderModel>(`/api/order`, aOrder);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,6 +136,10 @@ export class DataService {
|
|||||||
return this.http.post<UserItem>(`/api/user`, {});
|
return this.http.post<UserItem>(`/api/user`, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public putAccount(aAccount: UpdateAccountDto) {
|
||||||
|
return this.http.put<UserItem>(`/api/account/${aAccount.id}`, aAccount);
|
||||||
|
}
|
||||||
|
|
||||||
public putOrder(aOrder: UpdateOrderDto) {
|
public putOrder(aOrder: UpdateOrderDto) {
|
||||||
return this.http.put<UserItem>(`/api/order/${aOrder.id}`, aOrder);
|
return this.http.put<UserItem>(`/api/order/${aOrder.id}`, aOrder);
|
||||||
}
|
}
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 3.9 KiB |
Binary file not shown.
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 14 KiB |
BIN
apps/client/src/assets/badge-en-google-play.png
Normal file
BIN
apps/client/src/assets/badge-en-google-play.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"background_color": "transparent",
|
"background_color": "transparent",
|
||||||
"categories": ["finance", "utilities"],
|
"categories": ["finance", "utilities"],
|
||||||
"description": "Privacy-first Portfolio Tracker",
|
"description": "Open Source Portfolio Tracker",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
|
@ -6,14 +6,18 @@
|
|||||||
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io</loc>
|
<loc>https://ghostfol.io</loc>
|
||||||
<lastmod>2021-03-18T20:24:46+00:00</lastmod>
|
<lastmod>2021-05-14T20:24:46+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/about</loc>
|
<loc>https://ghostfol.io/about</loc>
|
||||||
<lastmod>2021-03-18T20:24:46+00:00</lastmod>
|
<lastmod>2021-05-14T20:24:46+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/pricing</loc>
|
||||||
|
<lastmod>2021-05-14T20:24:46+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/resources</loc>
|
<loc>https://ghostfol.io/resources</loc>
|
||||||
<lastmod>2021-03-18T20:24:46+00:00</lastmod>
|
<lastmod>2021-05-14T20:24:46+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
</urlset>
|
</urlset>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user