Compare commits
24 Commits
Author | SHA1 | Date | |
---|---|---|---|
d17b02092e | |||
c70eb7793e | |||
e3a1d2b9cf | |||
e68ebdf76d | |||
949cf58eef | |||
0816defb95 | |||
a076a1c933 | |||
40c95a541d | |||
2d04d7b8d5 | |||
94e0feac68 | |||
cd9e974c40 | |||
11700f75d9 | |||
0439a7beaa | |||
608b195ba9 | |||
8cb5fd64dd | |||
ef317a86ed | |||
19ada83d0b | |||
6ecf66ea2a | |||
65ba16c07c | |||
aee7c12c44 | |||
125956eb3e | |||
954224401d | |||
d268de3e12 | |||
39cfb4603b |
80
CHANGELOG.md
80
CHANGELOG.md
@ -5,14 +5,76 @@ 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).
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added a data source attribute to the transactions model
|
||||||
|
|
||||||
|
## 0.94.0 - 27.04.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the generic scraper symbols to the symbol lookup results
|
||||||
|
|
||||||
|
## 0.93.0 - 26.04.2021
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the user table styling of the admin control panel
|
||||||
|
- Improved the background colors in the dark mode
|
||||||
|
|
||||||
|
## 0.92.0 - 25.04.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Prepared further for multi accounts support: store account for new transactions
|
||||||
|
- Added a horizontal scrollbar to the user table of the admin control panel
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue in the header with outdated data
|
||||||
|
- Fixed an issue on the about page with outdated data
|
||||||
|
|
||||||
|
## 0.91.0 - 25.04.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Extended the support for feature flags to simplify the initial project setup
|
||||||
|
- Prepared for multi accounts support
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the styling of the rules in the _X-ray_ section
|
||||||
|
|
||||||
## 0.90.0 - 22.04.2021
|
## 0.90.0 - 22.04.2021
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Improved the user table of the admin control panel
|
|
||||||
- Added the symbol logo to the position detail dialog
|
- Added the symbol logo to the position detail dialog
|
||||||
- Introduced a third option for the market state: `delayed` (besides `open` and `closed`)
|
- Introduced a third option for the market state: `delayed` (besides `open` and `closed`)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the user table of the admin control panel
|
||||||
|
|
||||||
## 0.89.0 - 21.04.2021
|
## 0.89.0 - 21.04.2021
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@ -92,7 +154,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fixed an issue in the portfolio update on deleting a transaction
|
- Fixed an issue in the portfolio update on deleting a transaction
|
||||||
- Fixed an issue in the _X-Ray_ section (missing redirection on logout)
|
- Fixed an issue in the _X-ray_ section (missing redirection on logout)
|
||||||
|
|
||||||
## 0.82.0 - 10.04.2021
|
## 0.82.0 - 10.04.2021
|
||||||
|
|
||||||
@ -165,7 +227,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Grouped the _X-Ray_ section visually in _Currency Cluster Risk_ and _Platform Cluster Risk_
|
- Grouped the _X-ray_ section visually in _Currency Cluster Risk_ and _Platform Cluster Risk_
|
||||||
|
|
||||||
## 0.76.0 - 02.04.2021
|
## 0.76.0 - 02.04.2021
|
||||||
|
|
||||||
@ -177,7 +239,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fixed an issue in the _X-Ray_ section (empty portfolio)
|
- Fixed an issue in the _X-ray_ section (empty portfolio)
|
||||||
|
|
||||||
## 0.75.0 - 01.04.2021
|
## 0.75.0 - 01.04.2021
|
||||||
|
|
||||||
@ -190,7 +252,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added a _Create Account_ message in the _Live Demo_
|
- Added a _Create Account_ message in the _Live Demo_
|
||||||
- Added skeleton loaders to the _X-Ray_ section
|
- Added skeleton loaders to the _X-ray_ section
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
@ -206,7 +268,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Improved the intro text in the _X-Ray_ section
|
- Improved the intro text in the _X-ray_ section
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
@ -222,7 +284,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added an intro text to the _X-Ray_ section
|
- Added an intro text to the _X-ray_ section
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
@ -241,7 +303,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Improved the styling in the _X-Ray_ section
|
- Improved the styling in the _X-ray_ section
|
||||||
|
|
||||||
## 0.70.0 - 27.03.2021
|
## 0.70.0 - 27.03.2021
|
||||||
|
|
||||||
@ -249,7 +311,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
- Added the current _Fear & Greed Index_ as text
|
- Added the current _Fear & Greed Index_ as text
|
||||||
- Extended the landing page text: _Ghostfolio_ empowers busy folks...
|
- Extended the landing page text: _Ghostfolio_ empowers busy folks...
|
||||||
- Added the first static portfolio analysis rule in the brand new _X-Ray_ section
|
- Added the first static portfolio analysis rule in the brand new _X-ray_ section
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
213
apps/api/src/app/account/account.controller.ts
Normal file
213
apps/api/src/app/account/account.controller.ts
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
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 } 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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: { Platform: true },
|
||||||
|
orderBy: { name: 'desc' },
|
||||||
|
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 (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 {}
|
75
apps/api/src/app/account/account.service.ts
Normal file
75
apps/api/src/app/account/account.service.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Account, 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 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
35
apps/api/src/app/account/create-account.dto.ts
Normal file
35
apps/api/src/app/account/create-account.dto.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { Currency, DataSource, Type } from '@prisma/client';
|
||||||
|
import { IsISO8601, IsNumber, IsString, ValidateIf } from 'class-validator';
|
||||||
|
|
||||||
|
export class CreateAccountDto {
|
||||||
|
@IsString()
|
||||||
|
accountId: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
currency: Currency;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
dataSource: DataSource;
|
||||||
|
|
||||||
|
@IsISO8601()
|
||||||
|
date: string;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
fee: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@ValidateIf((object, value) => value !== null)
|
||||||
|
platformId: string | null;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
quantity: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
symbol: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
type: Type;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
unitPrice: number;
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
import { Order, Platform } from '@prisma/client';
|
||||||
|
|
||||||
|
export type OrderWithPlatform = Order & { Platform?: Platform };
|
38
apps/api/src/app/account/update-account.dto.ts
Normal file
38
apps/api/src/app/account/update-account.dto.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { Currency, DataSource, Type } from '@prisma/client';
|
||||||
|
import { IsISO8601, IsNumber, IsString, ValidateIf } from 'class-validator';
|
||||||
|
|
||||||
|
export class UpdateAccountDto {
|
||||||
|
@IsString()
|
||||||
|
accountId: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
currency: Currency;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
dataSource: DataSource;
|
||||||
|
|
||||||
|
@IsISO8601()
|
||||||
|
date: string;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
fee: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@ValidateIf((object, value) => value !== null)
|
||||||
|
platformId: string | null;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
quantity: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
symbol: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
type: Type;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
unitPrice: number;
|
||||||
|
}
|
@ -97,7 +97,7 @@ export class AdminService {
|
|||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
_count: {
|
_count: {
|
||||||
select: { Order: true }
|
select: { Account: true, Order: true }
|
||||||
},
|
},
|
||||||
alias: true,
|
alias: true,
|
||||||
Analytics: {
|
Analytics: {
|
||||||
|
@ -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(),
|
||||||
|
@ -36,7 +36,10 @@ export class ExperimentalService {
|
|||||||
const ordersWithPlatform: OrderWithPlatform[] = aOrders.map((order) => {
|
const ordersWithPlatform: OrderWithPlatform[] = aOrders.map((order) => {
|
||||||
return {
|
return {
|
||||||
...order,
|
...order,
|
||||||
|
accountId: undefined,
|
||||||
|
accountUserId: undefined,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
|
dataSource: undefined,
|
||||||
date: parseISO(order.date),
|
date: parseISO(order.date),
|
||||||
fee: 0,
|
fee: 0,
|
||||||
id: undefined,
|
id: undefined,
|
||||||
|
@ -26,7 +26,11 @@ export class InfoService {
|
|||||||
const globalPermissions: string[] = [];
|
const globalPermissions: string[] = [];
|
||||||
|
|
||||||
if (this.configurationService.get('ENABLE_FEATURE_SOCIAL_LOGIN')) {
|
if (this.configurationService.get('ENABLE_FEATURE_SOCIAL_LOGIN')) {
|
||||||
globalPermissions.push(permissions.useSocialLogin);
|
globalPermissions.push(permissions.enableSocialLogin);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
||||||
|
globalPermissions.push(permissions.enableSubscription);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
import { Settings, User } from '@prisma/client';
|
import { Account, Settings, User } from '@prisma/client';
|
||||||
|
|
||||||
export type UserWithSettings = User & { Settings: Settings };
|
export type UserWithSettings = User & {
|
||||||
|
Account: Account[];
|
||||||
|
Settings: Settings;
|
||||||
|
};
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
import { Currency, Type } from '@prisma/client';
|
import { Currency, DataSource, Type } from '@prisma/client';
|
||||||
import { IsISO8601, IsNumber, IsString, ValidateIf } from 'class-validator';
|
import { IsISO8601, IsNumber, IsString, ValidateIf } from 'class-validator';
|
||||||
|
|
||||||
export class CreateOrderDto {
|
export class CreateOrderDto {
|
||||||
|
@IsString()
|
||||||
|
accountId: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
currency: Currency;
|
currency: Currency;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
dataSource: DataSource;
|
||||||
|
|
||||||
@IsISO8601()
|
@IsISO8601()
|
||||||
date: string;
|
date: string;
|
||||||
|
|
||||||
|
@ -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 }
|
||||||
@ -118,6 +122,9 @@ export class OrderController {
|
|||||||
|
|
||||||
const date = parseISO(data.date);
|
const date = parseISO(data.date);
|
||||||
|
|
||||||
|
const accountId = data.accountId;
|
||||||
|
delete data.accountId;
|
||||||
|
|
||||||
if (data.platformId) {
|
if (data.platformId) {
|
||||||
const platformId = data.platformId;
|
const platformId = data.platformId;
|
||||||
delete data.platformId;
|
delete data.platformId;
|
||||||
@ -126,6 +133,11 @@ export class OrderController {
|
|||||||
{
|
{
|
||||||
...data,
|
...data,
|
||||||
date,
|
date,
|
||||||
|
Account: {
|
||||||
|
connect: {
|
||||||
|
id_userId: { id: accountId, userId: this.request.user.id }
|
||||||
|
}
|
||||||
|
},
|
||||||
Platform: { connect: { id: platformId } },
|
Platform: { connect: { id: platformId } },
|
||||||
User: { connect: { id: this.request.user.id } }
|
User: { connect: { id: this.request.user.id } }
|
||||||
},
|
},
|
||||||
@ -138,6 +150,11 @@ export class OrderController {
|
|||||||
{
|
{
|
||||||
...data,
|
...data,
|
||||||
date,
|
date,
|
||||||
|
Account: {
|
||||||
|
connect: {
|
||||||
|
id_userId: { id: accountId, userId: this.request.user.id }
|
||||||
|
}
|
||||||
|
},
|
||||||
User: { connect: { id: this.request.user.id } }
|
User: { connect: { id: this.request.user.id } }
|
||||||
},
|
},
|
||||||
this.request.user.id
|
this.request.user.id
|
||||||
@ -169,6 +186,9 @@ export class OrderController {
|
|||||||
|
|
||||||
const date = parseISO(data.date);
|
const date = parseISO(data.date);
|
||||||
|
|
||||||
|
const accountId = data.accountId;
|
||||||
|
delete data.accountId;
|
||||||
|
|
||||||
if (data.platformId) {
|
if (data.platformId) {
|
||||||
const platformId = data.platformId;
|
const platformId = data.platformId;
|
||||||
delete data.platformId;
|
delete data.platformId;
|
||||||
@ -178,6 +198,11 @@ export class OrderController {
|
|||||||
data: {
|
data: {
|
||||||
...data,
|
...data,
|
||||||
date,
|
date,
|
||||||
|
Account: {
|
||||||
|
connect: {
|
||||||
|
id_userId: { id: accountId, userId: this.request.user.id }
|
||||||
|
}
|
||||||
|
},
|
||||||
Platform: { connect: { id: platformId } },
|
Platform: { connect: { id: platformId } },
|
||||||
User: { connect: { id: this.request.user.id } }
|
User: { connect: { id: this.request.user.id } }
|
||||||
},
|
},
|
||||||
@ -199,6 +224,11 @@ export class OrderController {
|
|||||||
data: {
|
data: {
|
||||||
...data,
|
...data,
|
||||||
date,
|
date,
|
||||||
|
Account: {
|
||||||
|
connect: {
|
||||||
|
id_userId: { id: accountId, userId: this.request.user.id }
|
||||||
|
}
|
||||||
|
},
|
||||||
Platform: originalOrder.platformId
|
Platform: originalOrder.platformId
|
||||||
? { disconnect: true }
|
? { disconnect: true }
|
||||||
: undefined,
|
: undefined,
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
import { Currency, Type } from '@prisma/client';
|
import { Currency, DataSource, Type } from '@prisma/client';
|
||||||
import { IsISO8601, IsNumber, IsString, ValidateIf } from 'class-validator';
|
import { IsISO8601, IsNumber, IsString, ValidateIf } from 'class-validator';
|
||||||
|
|
||||||
export class UpdateOrderDto {
|
export class UpdateOrderDto {
|
||||||
|
@IsString()
|
||||||
|
accountId: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
currency: Currency;
|
currency: Currency;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
dataSource: DataSource;
|
||||||
|
|
||||||
@IsISO8601()
|
@IsISO8601()
|
||||||
date: string;
|
date: string;
|
||||||
|
|
||||||
|
@ -16,4 +16,5 @@ export interface Position {
|
|||||||
investmentInOriginalCurrency?: number;
|
investmentInOriginalCurrency?: number;
|
||||||
marketPrice?: number;
|
marketPrice?: number;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
transactionCount: number;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
|
import { Currency } from '@prisma/client';
|
||||||
|
|
||||||
export interface PortfolioPositionDetail {
|
export interface PortfolioPositionDetail {
|
||||||
averagePrice: number;
|
averagePrice: number;
|
||||||
currency: string;
|
currency: Currency;
|
||||||
firstBuyDate: string;
|
firstBuyDate: string;
|
||||||
grossPerformance: number;
|
grossPerformance: number;
|
||||||
grossPerformancePercent: number;
|
grossPerformancePercent: number;
|
||||||
@ -11,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 {
|
||||||
|
@ -20,6 +20,7 @@ export interface PortfolioPosition {
|
|||||||
sector?: string;
|
sector?: string;
|
||||||
shareCurrent: number;
|
shareCurrent: number;
|
||||||
shareInvestment: number;
|
shareInvestment: number;
|
||||||
|
transactionCount: number;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Currency } from '@prisma/client';
|
import { Currency, DataSource } from '@prisma/client';
|
||||||
|
|
||||||
export interface SymbolItem {
|
export interface SymbolItem {
|
||||||
currency: Currency;
|
currency: Currency;
|
||||||
|
dataSource: DataSource;
|
||||||
marketPrice: number;
|
marketPrice: number;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
||||||
|
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
||||||
import { convertFromYahooSymbol } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
|
import { convertFromYahooSymbol } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Currency } from '@prisma/client';
|
import { Currency } from '@prisma/client';
|
||||||
@ -10,31 +12,51 @@ import { SymbolItem } from './interfaces/symbol-item.interface';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class SymbolService {
|
export class SymbolService {
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly dataProviderService: DataProviderService
|
private readonly dataProviderService: DataProviderService,
|
||||||
|
private readonly ghostfolioScraperApiService: GhostfolioScraperApiService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async get(aSymbol: string): Promise<SymbolItem> {
|
public async get(aSymbol: string): Promise<SymbolItem> {
|
||||||
const response = await this.dataProviderService.get([aSymbol]);
|
const response = await this.dataProviderService.get([aSymbol]);
|
||||||
const { currency, marketPrice } = response[aSymbol];
|
const { currency, dataSource, marketPrice } = response[aSymbol];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
dataSource,
|
||||||
marketPrice,
|
marketPrice,
|
||||||
currency: <Currency>(<unknown>currency)
|
currency: <Currency>(<unknown>currency)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async lookup(aQuery: string): Promise<LookupItem[]> {
|
public async lookup(aQuery = ''): Promise<LookupItem[]> {
|
||||||
|
const query = aQuery.toLowerCase();
|
||||||
|
const results: LookupItem[] = [];
|
||||||
|
|
||||||
|
if (!query) {
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
const get = bent(
|
const get = bent(
|
||||||
`https://query1.finance.yahoo.com/v1/finance/search?q=${aQuery}&lang=en-US®ion=US"esCount=8&newsCount=0&enableFuzzyQuery=false"esQueryId=tss_match_phrase_query&multiQuoteQueryId=multi_quote_single_token_query&newsQueryId=news_cie_vespa&enableCb=true&enableNavLinks=false&enableEnhancedTrivialQuery=true`,
|
`https://query1.finance.yahoo.com/v1/finance/search?q=${query}&lang=en-US®ion=US"esCount=8&newsCount=0&enableFuzzyQuery=false"esQueryId=tss_match_phrase_query&multiQuoteQueryId=multi_quote_single_token_query&newsQueryId=news_cie_vespa&enableCb=true&enableNavLinks=false&enableEnhancedTrivialQuery=true`,
|
||||||
'GET',
|
'GET',
|
||||||
'json',
|
'json',
|
||||||
200
|
200
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Add custom symbols
|
||||||
|
const scraperConfigurations = await this.ghostfolioScraperApiService.getScraperConfigurations();
|
||||||
|
scraperConfigurations.forEach((scraperConfiguration) => {
|
||||||
|
if (scraperConfiguration.name.toLowerCase().startsWith(query)) {
|
||||||
|
results.push({
|
||||||
|
name: scraperConfiguration.name,
|
||||||
|
symbol: scraperConfiguration.symbol
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { quotes } = await get();
|
const { quotes } = await get();
|
||||||
|
|
||||||
return quotes
|
const searchResult = quotes
|
||||||
.filter(({ isYahooFinance }) => {
|
.filter(({ isYahooFinance }) => {
|
||||||
return isYahooFinance;
|
return isYahooFinance;
|
||||||
})
|
})
|
||||||
@ -59,6 +81,8 @@ export class SymbolService {
|
|||||||
symbol: convertFromYahooSymbol(symbol)
|
symbol: convertFromYahooSymbol(symbol)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return results.concat(searchResult);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { Currency } from '@prisma/client';
|
import { Account, Currency } from '@prisma/client';
|
||||||
|
|
||||||
import { Access } from './access.interface';
|
import { Access } from './access.interface';
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
access: Access[];
|
access: Access[];
|
||||||
|
accounts: Account[];
|
||||||
alias?: string;
|
alias?: string;
|
||||||
id: string;
|
id: string;
|
||||||
permissions: string[];
|
permissions: string[];
|
||||||
|
@ -25,6 +25,7 @@ export class UserService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async getUser({
|
public async getUser({
|
||||||
|
Account,
|
||||||
alias,
|
alias,
|
||||||
id,
|
id,
|
||||||
role,
|
role,
|
||||||
@ -53,6 +54,7 @@ export class UserService {
|
|||||||
id: accessItem.id
|
id: accessItem.id
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
accounts: Account,
|
||||||
permissions: currentPermissions,
|
permissions: currentPermissions,
|
||||||
settings: {
|
settings: {
|
||||||
baseCurrency: Settings?.currency || UserService.DEFAULT_CURRENCY,
|
baseCurrency: Settings?.currency || UserService.DEFAULT_CURRENCY,
|
||||||
@ -69,7 +71,7 @@ export class UserService {
|
|||||||
userWhereUniqueInput: Prisma.UserWhereUniqueInput
|
userWhereUniqueInput: Prisma.UserWhereUniqueInput
|
||||||
): Promise<UserWithSettings | null> {
|
): Promise<UserWithSettings | null> {
|
||||||
const user = await this.prisma.user.findUnique({
|
const user = await this.prisma.user.findUnique({
|
||||||
include: { Settings: true },
|
include: { Account: true, Settings: true },
|
||||||
where: userWhereUniqueInput
|
where: userWhereUniqueInput
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -116,7 +118,15 @@ export class UserService {
|
|||||||
|
|
||||||
public async createUser(data?: Prisma.UserCreateInput): Promise<User> {
|
public async createUser(data?: Prisma.UserCreateInput): Promise<User> {
|
||||||
let user = await this.prisma.user.create({
|
let user = await this.prisma.user.create({
|
||||||
data
|
data: {
|
||||||
|
...data,
|
||||||
|
Account: {
|
||||||
|
create: {
|
||||||
|
isDefault: true,
|
||||||
|
name: 'Default Account'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.provider === Provider.ANONYMOUS) {
|
if (data.provider === Provider.ANONYMOUS) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { baseCurrency, getUtc, getYesterday } from '@ghostfolio/helper';
|
import { 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';
|
||||||
@ -14,6 +14,9 @@ import { PrismaService } from '../services/prisma.service';
|
|||||||
import { RulesService } from '../services/rules.service';
|
import { RulesService } from '../services/rules.service';
|
||||||
import { Portfolio } from './portfolio';
|
import { Portfolio } from './portfolio';
|
||||||
|
|
||||||
|
const DEFAULT_ACCOUNT_ID = '693a834b-eb89-42c9-ae47-35196c25d269';
|
||||||
|
const USER_ID = 'ca6ce867-5d31-495a-bce9-5942bbca9237';
|
||||||
|
|
||||||
describe('Portfolio', () => {
|
describe('Portfolio', () => {
|
||||||
let alphaVantageService: AlphaVantageService;
|
let alphaVantageService: AlphaVantageService;
|
||||||
let configurationService: ConfigurationService;
|
let configurationService: ConfigurationService;
|
||||||
@ -67,15 +70,27 @@ 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: '',
|
id: USER_ID,
|
||||||
provider: null,
|
provider: null,
|
||||||
role: Role.USER,
|
role: Role.USER,
|
||||||
Settings: {
|
Settings: {
|
||||||
currency: Currency.CHF,
|
currency: Currency.CHF,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
userId: ''
|
userId: USER_ID
|
||||||
},
|
},
|
||||||
thirdPartyId: null,
|
thirdPartyId: null,
|
||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
@ -126,8 +141,11 @@ describe('Portfolio', () => {
|
|||||||
it('should return ["BTC"]', async () => {
|
it('should return ["BTC"]', async () => {
|
||||||
await portfolio.setOrders([
|
await portfolio.setOrders([
|
||||||
{
|
{
|
||||||
|
accountId: DEFAULT_ACCOUNT_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',
|
||||||
@ -137,7 +155,7 @@ describe('Portfolio', () => {
|
|||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
unitPrice: 49631.24,
|
unitPrice: 49631.24,
|
||||||
updatedAt: null,
|
updatedAt: null,
|
||||||
userId: null
|
userId: USER_ID
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -182,6 +200,7 @@ describe('Portfolio', () => {
|
|||||||
// shareCurrent: 0.9999999559148652,
|
// shareCurrent: 0.9999999559148652,
|
||||||
shareInvestment: 1,
|
shareInvestment: 1,
|
||||||
symbol: 'BTCUSD',
|
symbol: 'BTCUSD',
|
||||||
|
transactionCount: 0,
|
||||||
type: 'Cryptocurrency'
|
type: 'Cryptocurrency'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -224,8 +243,11 @@ describe('Portfolio', () => {
|
|||||||
it('should return ["ETHUSD"]', async () => {
|
it('should return ["ETHUSD"]', async () => {
|
||||||
await portfolio.setOrders([
|
await portfolio.setOrders([
|
||||||
{
|
{
|
||||||
|
accountId: DEFAULT_ACCOUNT_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',
|
||||||
@ -235,7 +257,7 @@ describe('Portfolio', () => {
|
|||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
unitPrice: 991.49,
|
unitPrice: 991.49,
|
||||||
updatedAt: null,
|
updatedAt: null,
|
||||||
userId: null
|
userId: USER_ID
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -276,8 +298,9 @@ describe('Portfolio', () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
quantity: 0.2,
|
quantity: 0.2,
|
||||||
shareCurrent: 1,
|
// shareCurrent: 1,
|
||||||
shareInvestment: 1,
|
shareInvestment: 1,
|
||||||
|
transactionCount: 1,
|
||||||
symbol: 'ETHUSD',
|
symbol: 'ETHUSD',
|
||||||
type: 'Cryptocurrency'
|
type: 'Cryptocurrency'
|
||||||
}
|
}
|
||||||
@ -316,8 +339,11 @@ describe('Portfolio', () => {
|
|||||||
it('should return ["ETHUSD"]', async () => {
|
it('should return ["ETHUSD"]', async () => {
|
||||||
await portfolio.setOrders([
|
await portfolio.setOrders([
|
||||||
{
|
{
|
||||||
|
accountId: DEFAULT_ACCOUNT_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',
|
||||||
@ -327,11 +353,14 @@ describe('Portfolio', () => {
|
|||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
unitPrice: 991.49,
|
unitPrice: 991.49,
|
||||||
updatedAt: null,
|
updatedAt: null,
|
||||||
userId: null
|
userId: USER_ID
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
accountId: DEFAULT_ACCOUNT_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',
|
||||||
@ -341,7 +370,7 @@ describe('Portfolio', () => {
|
|||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
unitPrice: 1050,
|
unitPrice: 1050,
|
||||||
updatedAt: null,
|
updatedAt: null,
|
||||||
userId: null
|
userId: USER_ID
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -388,8 +417,11 @@ describe('Portfolio', () => {
|
|||||||
it('should return ["BTCUSD", "ETHUSD"]', async () => {
|
it('should return ["BTCUSD", "ETHUSD"]', async () => {
|
||||||
await portfolio.setOrders([
|
await portfolio.setOrders([
|
||||||
{
|
{
|
||||||
|
accountId: DEFAULT_ACCOUNT_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',
|
||||||
@ -399,11 +431,14 @@ describe('Portfolio', () => {
|
|||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
unitPrice: 3562.089535970158,
|
unitPrice: 3562.089535970158,
|
||||||
updatedAt: null,
|
updatedAt: null,
|
||||||
userId: null
|
userId: USER_ID
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
accountId: DEFAULT_ACCOUNT_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',
|
||||||
@ -413,7 +448,7 @@ describe('Portfolio', () => {
|
|||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
unitPrice: 991.49,
|
unitPrice: 991.49,
|
||||||
updatedAt: null,
|
updatedAt: null,
|
||||||
userId: null
|
userId: USER_ID
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -473,8 +508,11 @@ describe('Portfolio', () => {
|
|||||||
it('should work with buy and sell', async () => {
|
it('should work with buy and sell', async () => {
|
||||||
await portfolio.setOrders([
|
await portfolio.setOrders([
|
||||||
{
|
{
|
||||||
|
accountId: DEFAULT_ACCOUNT_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',
|
||||||
@ -484,11 +522,14 @@ describe('Portfolio', () => {
|
|||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
unitPrice: 991.49,
|
unitPrice: 991.49,
|
||||||
updatedAt: null,
|
updatedAt: null,
|
||||||
userId: null
|
userId: USER_ID
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
accountId: DEFAULT_ACCOUNT_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',
|
||||||
@ -498,11 +539,14 @@ describe('Portfolio', () => {
|
|||||||
type: Type.SELL,
|
type: Type.SELL,
|
||||||
unitPrice: 1050,
|
unitPrice: 1050,
|
||||||
updatedAt: null,
|
updatedAt: null,
|
||||||
userId: null
|
userId: USER_ID
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
accountId: DEFAULT_ACCOUNT_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',
|
||||||
@ -512,7 +556,7 @@ describe('Portfolio', () => {
|
|||||||
type: Type.BUY,
|
type: Type.BUY,
|
||||||
unitPrice: 1050,
|
unitPrice: 1050,
|
||||||
updatedAt: null,
|
updatedAt: null,
|
||||||
userId: null
|
userId: USER_ID
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -57,7 +57,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 +82,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
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -289,7 +291,9 @@ export class Portfolio implements PortfolioInterface {
|
|||||||
data[symbol]?.currency,
|
data[symbol]?.currency,
|
||||||
this.user.Settings.currency
|
this.user.Settings.currency
|
||||||
) / value,
|
) / value,
|
||||||
shareInvestment: portfolioItem.positions[symbol].investment / investment
|
shareInvestment:
|
||||||
|
portfolioItem.positions[symbol].investment / investment,
|
||||||
|
transactionCount: portfolioItem.positions[symbol].transactionCount
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -582,7 +586,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 +628,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 +736,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
|
||||||
|
@ -15,6 +15,7 @@ export class ConfigurationService {
|
|||||||
ENABLE_FEATURE_CUSTOM_SYMBOLS: bool({ default: false }),
|
ENABLE_FEATURE_CUSTOM_SYMBOLS: bool({ default: false }),
|
||||||
ENABLE_FEATURE_FEAR_AND_GREED_INDEX: bool({ default: false }),
|
ENABLE_FEATURE_FEAR_AND_GREED_INDEX: bool({ default: false }),
|
||||||
ENABLE_FEATURE_SOCIAL_LOGIN: bool({ default: false }),
|
ENABLE_FEATURE_SOCIAL_LOGIN: bool({ default: false }),
|
||||||
|
ENABLE_FEATURE_SUBSCRIPTION: bool({ default: false }),
|
||||||
GOOGLE_CLIENT_ID: str({ default: 'dummyClientId' }),
|
GOOGLE_CLIENT_ID: str({ default: 'dummyClientId' }),
|
||||||
GOOGLE_SECRET: str({ default: 'dummySecret' }),
|
GOOGLE_SECRET: str({ default: 'dummySecret' }),
|
||||||
JWT_SECRET_KEY: str({}),
|
JWT_SECRET_KEY: str({}),
|
||||||
|
@ -18,6 +18,7 @@ import {
|
|||||||
|
|
||||||
import { ConfigurationService } from './configuration.service';
|
import { ConfigurationService } from './configuration.service';
|
||||||
import { DataProviderService } from './data-provider.service';
|
import { DataProviderService } from './data-provider.service';
|
||||||
|
import { GhostfolioScraperApiService } from './data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
||||||
import { PrismaService } from './prisma.service';
|
import { PrismaService } from './prisma.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -25,6 +26,7 @@ export class DataGatheringService {
|
|||||||
public constructor(
|
public constructor(
|
||||||
private readonly configurationService: ConfigurationService,
|
private readonly configurationService: ConfigurationService,
|
||||||
private readonly dataProviderService: DataProviderService,
|
private readonly dataProviderService: DataProviderService,
|
||||||
|
private readonly ghostfolioScraperApi: GhostfolioScraperApiService,
|
||||||
private prisma: PrismaService
|
private prisma: PrismaService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -183,6 +185,17 @@ export class DataGatheringService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getCustomSymbolsToGather(startDate?: Date) {
|
||||||
|
const scraperConfigurations = await this.ghostfolioScraperApi.getScraperConfigurations();
|
||||||
|
|
||||||
|
return scraperConfigurations.map((scraperConfiguration) => {
|
||||||
|
return {
|
||||||
|
date: startDate,
|
||||||
|
symbol: scraperConfiguration.symbol
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private getBenchmarksToGather(startDate: Date) {
|
private getBenchmarksToGather(startDate: Date) {
|
||||||
const benchmarksToGather = benchmarks.map((symbol) => {
|
const benchmarksToGather = benchmarks.map((symbol) => {
|
||||||
return {
|
return {
|
||||||
@ -201,32 +214,6 @@ export class DataGatheringService {
|
|||||||
return benchmarksToGather;
|
return benchmarksToGather;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getCustomSymbolsToGather(startDate: Date) {
|
|
||||||
const customSymbolsToGather = [];
|
|
||||||
|
|
||||||
if (this.configurationService.get('ENABLE_FEATURE_CUSTOM_SYMBOLS')) {
|
|
||||||
try {
|
|
||||||
const {
|
|
||||||
value: scraperConfigString
|
|
||||||
} = await this.prisma.property.findFirst({
|
|
||||||
select: {
|
|
||||||
value: true
|
|
||||||
},
|
|
||||||
where: { key: 'SCRAPER_CONFIG' }
|
|
||||||
});
|
|
||||||
|
|
||||||
JSON.parse(scraperConfigString).forEach((item) => {
|
|
||||||
customSymbolsToGather.push({
|
|
||||||
date: startDate,
|
|
||||||
symbol: item.symbol
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
return customSymbolsToGather;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getSymbols7D(): Promise<{ date: Date; symbol: string }[]> {
|
private async getSymbols7D(): Promise<{ date: Date; symbol: string }[]> {
|
||||||
const startDate = subDays(resetHours(new Date()), 7);
|
const startDate = subDays(resetHours(new Date()), 7);
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
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';
|
||||||
@ -12,9 +13,12 @@ import {
|
|||||||
MarketState
|
MarketState
|
||||||
} from '../../interfaces/interfaces';
|
} from '../../interfaces/interfaces';
|
||||||
import { PrismaService } from '../../prisma.service';
|
import { PrismaService } from '../../prisma.service';
|
||||||
|
import { ScraperConfig } from './interfaces/scraper-config.interface';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GhostfolioScraperApiService implements DataProviderInterface {
|
export class GhostfolioScraperApiService implements DataProviderInterface {
|
||||||
|
private static NUMERIC_REGEXP = /[-]{0,1}[\d]*[.,]{0,1}[\d]+/g;
|
||||||
|
|
||||||
public constructor(private prisma: PrismaService) {}
|
public constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
public async get(
|
public async get(
|
||||||
@ -27,7 +31,7 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
|||||||
try {
|
try {
|
||||||
const symbol = aSymbols[0];
|
const symbol = aSymbols[0];
|
||||||
|
|
||||||
const scraperConfig = await this.getScraperConfig(symbol);
|
const scraperConfig = await this.getScraperConfigurationBySymbol(symbol);
|
||||||
|
|
||||||
const { marketPrice } = await this.prisma.marketData.findFirst({
|
const { marketPrice } = await this.prisma.marketData.findFirst({
|
||||||
orderBy: {
|
orderBy: {
|
||||||
@ -42,6 +46,7 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
|||||||
[symbol]: {
|
[symbol]: {
|
||||||
marketPrice,
|
marketPrice,
|
||||||
currency: scraperConfig?.currency,
|
currency: scraperConfig?.currency,
|
||||||
|
dataSource: DataSource.GHOSTFOLIO,
|
||||||
marketState: MarketState.delayed,
|
marketState: MarketState.delayed,
|
||||||
name: scraperConfig?.name
|
name: scraperConfig?.name
|
||||||
}
|
}
|
||||||
@ -68,19 +73,18 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
|||||||
try {
|
try {
|
||||||
const symbol = aSymbols[0];
|
const symbol = aSymbols[0];
|
||||||
|
|
||||||
const scraperConfig = await this.getScraperConfig(symbol);
|
const scraperConfiguration = await this.getScraperConfigurationBySymbol(
|
||||||
|
symbol
|
||||||
|
);
|
||||||
|
|
||||||
const get = bent(scraperConfig?.url, 'GET', 'string', 200, {});
|
const get = bent(scraperConfiguration?.url, 'GET', 'string', 200, {});
|
||||||
|
|
||||||
const html = await get();
|
const html = await get();
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
|
|
||||||
const string = $(scraperConfig?.selector)
|
const value = this.extractNumberFromString(
|
||||||
.text()
|
$(scraperConfiguration?.selector).text()
|
||||||
.replace('CHF', '')
|
);
|
||||||
.trim();
|
|
||||||
|
|
||||||
const value = parseFloat(string);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
[symbol]: {
|
[symbol]: {
|
||||||
@ -96,7 +100,7 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getScraperConfig(aSymbol: string) {
|
public async getScraperConfigurations(): Promise<ScraperConfig[]> {
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
value: scraperConfigString
|
value: scraperConfigString
|
||||||
@ -107,11 +111,27 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
|||||||
where: { key: 'SCRAPER_CONFIG' }
|
where: { key: 'SCRAPER_CONFIG' }
|
||||||
});
|
});
|
||||||
|
|
||||||
return JSON.parse(scraperConfigString).find((item) => {
|
return JSON.parse(scraperConfigString);
|
||||||
return item.symbol === aSymbol;
|
|
||||||
});
|
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
return {};
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractNumberFromString(aString: string): number {
|
||||||
|
try {
|
||||||
|
const [numberString] = aString.match(
|
||||||
|
GhostfolioScraperApiService.NUMERIC_REGEXP
|
||||||
|
);
|
||||||
|
return parseFloat(numberString.trim());
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getScraperConfigurationBySymbol(aSymbol: string) {
|
||||||
|
const scraperConfigurations = await this.getScraperConfigurations();
|
||||||
|
return scraperConfigurations.find((scraperConfiguration) => {
|
||||||
|
return scraperConfiguration.symbol === aSymbol;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
import { Currency } from '@prisma/client';
|
||||||
|
|
||||||
|
export interface ScraperConfig {
|
||||||
|
currency: Currency;
|
||||||
|
name: string;
|
||||||
|
selector: string;
|
||||||
|
symbol: string;
|
||||||
|
url: string;
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
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';
|
||||||
|
|
||||||
@ -39,6 +40,7 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
|||||||
return {
|
return {
|
||||||
'GF.FEAR_AND_GREED_INDEX': {
|
'GF.FEAR_AND_GREED_INDEX': {
|
||||||
currency: undefined,
|
currency: undefined,
|
||||||
|
dataSource: DataSource.RAKUTEN,
|
||||||
marketPrice: fgi.now.value,
|
marketPrice: fgi.now.value,
|
||||||
marketState: MarketState.open,
|
marketState: MarketState.open,
|
||||||
name: RakutenRapidApiService.FEAR_AND_GREED_INDEX_NAME
|
name: RakutenRapidApiService.FEAR_AND_GREED_INDEX_NAME
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { isCrypto, isCurrency, parseCurrency } from '@ghostfolio/helper';
|
import { 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';
|
||||||
|
|
||||||
@ -49,6 +50,7 @@ export class YahooFinanceService implements DataProviderInterface {
|
|||||||
|
|
||||||
response[symbol] = {
|
response[symbol] = {
|
||||||
currency: parseCurrency(value.price?.currency),
|
currency: parseCurrency(value.price?.currency),
|
||||||
|
dataSource: DataSource.YAHOO,
|
||||||
exchange: this.parseExchange(value.price?.exchangeName),
|
exchange: this.parseExchange(value.price?.exchangeName),
|
||||||
marketState:
|
marketState:
|
||||||
value.price?.marketState === 'REGULAR' || isCrypto(symbol)
|
value.price?.marketState === 'REGULAR' || isCrypto(symbol)
|
||||||
|
@ -7,6 +7,7 @@ export interface Environment extends CleanedEnvAccessors {
|
|||||||
ENABLE_FEATURE_CUSTOM_SYMBOLS: boolean;
|
ENABLE_FEATURE_CUSTOM_SYMBOLS: boolean;
|
||||||
ENABLE_FEATURE_FEAR_AND_GREED_INDEX: boolean;
|
ENABLE_FEATURE_FEAR_AND_GREED_INDEX: boolean;
|
||||||
ENABLE_FEATURE_SOCIAL_LOGIN: boolean;
|
ENABLE_FEATURE_SOCIAL_LOGIN: boolean;
|
||||||
|
ENABLE_FEATURE_SUBSCRIPTION: boolean;
|
||||||
GOOGLE_CLIENT_ID: string;
|
GOOGLE_CLIENT_ID: string;
|
||||||
GOOGLE_SECRET: string;
|
GOOGLE_SECRET: string;
|
||||||
JWT_SECRET_KEY: string;
|
JWT_SECRET_KEY: string;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Currency, Platform } from '@prisma/client';
|
import { Currency, DataSource, Platform } from '@prisma/client';
|
||||||
|
|
||||||
import { OrderType } from '../../models/order-type';
|
import { OrderType } from '../../models/order-type';
|
||||||
|
|
||||||
@ -51,6 +51,7 @@ export interface IDataProviderHistoricalResponse {
|
|||||||
|
|
||||||
export interface IDataProviderResponse {
|
export interface IDataProviderResponse {
|
||||||
currency: Currency;
|
currency: Currency;
|
||||||
|
dataSource: DataSource;
|
||||||
exchange?: string;
|
exchange?: string;
|
||||||
industry?: Industry;
|
industry?: Industry;
|
||||||
marketChange?: number;
|
marketChange?: number;
|
||||||
|
@ -2,7 +2,8 @@ import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
|||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PrismaService extends PrismaClient
|
export class PrismaService
|
||||||
|
extends PrismaClient
|
||||||
implements OnModuleInit, OnModuleDestroy {
|
implements OnModuleInit, OnModuleDestroy {
|
||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
await this.$connect();
|
await this.$connect();
|
||||||
|
@ -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: () =>
|
||||||
@ -21,6 +16,18 @@ const routes: Routes = [
|
|||||||
(m) => m.AccountPageModule
|
(m) => m.AccountPageModule
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'accounts',
|
||||||
|
loadChildren: () =>
|
||||||
|
import('./pages/accounts/accounts-page.module').then(
|
||||||
|
(m) => m.AccountsPageModule
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'admin',
|
||||||
|
loadChildren: () =>
|
||||||
|
import('./pages/admin/admin-page.module').then((m) => m.AdminPageModule)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'auth',
|
path: 'auth',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
|
@ -74,7 +74,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();
|
||||||
|
@ -0,0 +1,86 @@
|
|||||||
|
<table
|
||||||
|
class="w-100"
|
||||||
|
matSort
|
||||||
|
matSortActive="account"
|
||||||
|
matSortDirection="desc"
|
||||||
|
mat-table
|
||||||
|
[dataSource]="dataSource"
|
||||||
|
>
|
||||||
|
<ng-container matColumnDef="account">
|
||||||
|
<th *matHeaderCellDef i18n mat-header-cell mat-sort-header>Name</th>
|
||||||
|
<td *matCellDef="let element" mat-cell>
|
||||||
|
{{ element.name }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="type">
|
||||||
|
<th
|
||||||
|
*matHeaderCellDef
|
||||||
|
class="d-none d-lg-table-cell justify-content-center"
|
||||||
|
i18n
|
||||||
|
mat-header-cell
|
||||||
|
mat-sort-header
|
||||||
|
>
|
||||||
|
Type
|
||||||
|
</th>
|
||||||
|
<td
|
||||||
|
mat-cell
|
||||||
|
*matCellDef="let element"
|
||||||
|
class="d-none d-lg-table-cell text-center"
|
||||||
|
>
|
||||||
|
<div class="d-inline-flex justify-content-center px-2 py-1 type-badge">
|
||||||
|
<span>{{ element.accountType }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="platform">
|
||||||
|
<th *matHeaderCellDef i18n mat-header-cell mat-sort-header>Platform</th>
|
||||||
|
<td mat-cell *matCellDef="let element">
|
||||||
|
<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-0 text-center" i18n mat-header-cell></th>
|
||||||
|
<td *matCellDef="let element" class="px-0 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 (click)="onDeleteAccount(element.id)">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</mat-menu>
|
||||||
|
</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,59 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
::ng-deep {
|
||||||
|
.mat-form-field-infix {
|
||||||
|
border-top: 0 solid transparent !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-table {
|
||||||
|
td {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
::ng-deep {
|
||||||
|
.mat-sort-header-container {
|
||||||
|
justify-content: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-row {
|
||||||
|
&:nth-child(even) {
|
||||||
|
background-color: rgba(
|
||||||
|
var(--dark-primary-text),
|
||||||
|
var(--palette-background-hover-alpha)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-badge {
|
||||||
|
background-color: rgba(var(--dark-primary-text), 0.05);
|
||||||
|
border-radius: 1rem;
|
||||||
|
line-height: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(.is-dark-theme) {
|
||||||
|
.mat-form-field {
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-badge {
|
||||||
|
background-color: rgba(var(--light-primary-text), 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 { Order as OrderModel } 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: OrderModel[];
|
||||||
|
@Input() baseCurrency: string;
|
||||||
|
@Input() deviceType: string;
|
||||||
|
@Input() locale: string;
|
||||||
|
@Input() showActions: boolean;
|
||||||
|
|
||||||
|
@Output() accountDeleted = new EventEmitter<string>();
|
||||||
|
@Output() accountToUpdate = new EventEmitter<OrderModel>();
|
||||||
|
|
||||||
|
@ViewChild(MatSort) sort: MatSort;
|
||||||
|
|
||||||
|
public dataSource: MatTableDataSource<OrderModel> = 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', 'type', 'platform'];
|
||||||
|
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
|
if (this.showActions) {
|
||||||
|
this.displayedColumns.push('actions');
|
||||||
|
}
|
||||||
|
|
||||||
|
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: OrderModel) {
|
||||||
|
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 {}
|
@ -37,7 +37,15 @@
|
|||||||
>Transactions</a
|
>Transactions</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
*ngIf="canAccessAdminAccessControl"
|
class="d-none d-sm-block mx-1"
|
||||||
|
[routerLink]="['/accounts']"
|
||||||
|
i18n
|
||||||
|
mat-flat-button
|
||||||
|
[color]="currentRoute === 'accounts' ? 'primary' : null"
|
||||||
|
>Accounts</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
*ngIf="hasPermissionToAccessAdminControl"
|
||||||
class="d-none d-sm-block mx-1"
|
class="d-none d-sm-block mx-1"
|
||||||
[routerLink]="['/admin']"
|
[routerLink]="['/admin']"
|
||||||
i18n
|
i18n
|
||||||
@ -141,6 +149,14 @@
|
|||||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'transactions' }"
|
[ngClass]="{ 'font-weight-bold': currentRoute === 'transactions' }"
|
||||||
>Transactions</a
|
>Transactions</a
|
||||||
>
|
>
|
||||||
|
<a
|
||||||
|
class="d-block d-sm-none"
|
||||||
|
[routerLink]="['/accounts']"
|
||||||
|
i18n
|
||||||
|
mat-menu-item
|
||||||
|
[ngClass]="{ 'font-weight-bold': currentRoute === 'accounts' }"
|
||||||
|
>Accounts</a
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
class="align-items-center d-flex"
|
class="align-items-center d-flex"
|
||||||
[routerLink]="['/account']"
|
[routerLink]="['/account']"
|
||||||
@ -150,7 +166,7 @@
|
|||||||
>Account</a
|
>Account</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
*ngIf="canAccessAdminAccessControl"
|
*ngIf="hasPermissionToAccessAdminControl"
|
||||||
class="d-block d-sm-none"
|
class="d-block d-sm-none"
|
||||||
[routerLink]="['/admin']"
|
[routerLink]="['/admin']"
|
||||||
i18n
|
i18n
|
||||||
|
@ -27,8 +27,8 @@ export class HeaderComponent implements OnChanges {
|
|||||||
@Input() info: InfoItem;
|
@Input() info: InfoItem;
|
||||||
@Input() user: User;
|
@Input() user: User;
|
||||||
|
|
||||||
public canAccessAdminAccessControl: boolean;
|
public hasPermissionToAccessAdminControl: boolean;
|
||||||
public hasPermissionToUseSocialLogin: boolean;
|
public hasPermissionForSocialLogin: boolean;
|
||||||
public impersonationId: string;
|
public impersonationId: string;
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
@ -49,15 +49,15 @@ export class HeaderComponent implements OnChanges {
|
|||||||
|
|
||||||
public ngOnChanges() {
|
public ngOnChanges() {
|
||||||
if (this.user) {
|
if (this.user) {
|
||||||
this.canAccessAdminAccessControl = hasPermission(
|
this.hasPermissionToAccessAdminControl = hasPermission(
|
||||||
this.user.permissions,
|
this.user.permissions,
|
||||||
permissions.accessAdminControl
|
permissions.accessAdminControl
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.hasPermissionToUseSocialLogin = hasPermission(
|
this.hasPermissionForSocialLogin = hasPermission(
|
||||||
this.info?.globalPermissions,
|
this.info?.globalPermissions,
|
||||||
permissions.useSocialLogin
|
permissions.enableSocialLogin
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,7 +81,7 @@ export class HeaderComponent implements OnChanges {
|
|||||||
autoFocus: false,
|
autoFocus: false,
|
||||||
data: {
|
data: {
|
||||||
accessToken: '',
|
accessToken: '',
|
||||||
hasPermissionToUseSocialLogin: this.hasPermissionToUseSocialLogin
|
hasPermissionToUseSocialLogin: this.hasPermissionForSocialLogin
|
||||||
},
|
},
|
||||||
width: '30rem'
|
width: '30rem'
|
||||||
});
|
});
|
||||||
|
@ -34,16 +34,16 @@
|
|||||||
<div *ngIf="showDetails" class="row">
|
<div *ngIf="showDetails" class="row">
|
||||||
<div class="d-flex col justify-content-end">
|
<div class="d-flex col justify-content-end">
|
||||||
<gf-value
|
<gf-value
|
||||||
colorizeSign="true"
|
[colorizeSign]="true"
|
||||||
isCurrency="true"
|
[isCurrency]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[value]="isLoading ? undefined : performance?.currentNetPerformance"
|
[value]="isLoading ? undefined : performance?.currentNetPerformance"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<gf-value
|
<gf-value
|
||||||
colorizeSign="true"
|
[colorizeSign]="true"
|
||||||
isPercent="true"
|
[isPercent]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[value]="
|
[value]="
|
||||||
isLoading ? undefined : performance?.currentNetPerformancePercent
|
isLoading ? undefined : performance?.currentNetPerformancePercent
|
||||||
|
@ -16,17 +16,17 @@
|
|||||||
<div class="d-flex flex-column flex-wrap justify-content-end">
|
<div class="d-flex flex-column flex-wrap justify-content-end">
|
||||||
<gf-value
|
<gf-value
|
||||||
class="justify-content-end mb-2"
|
class="justify-content-end mb-2"
|
||||||
colorizeSign="true"
|
|
||||||
position="end"
|
position="end"
|
||||||
|
[colorizeSign]="true"
|
||||||
[currency]="baseCurrency"
|
[currency]="baseCurrency"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[value]="isLoading ? undefined : performance?.currentGrossPerformance"
|
[value]="isLoading ? undefined : performance?.currentGrossPerformance"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
<gf-value
|
<gf-value
|
||||||
class="justify-content-end"
|
class="justify-content-end"
|
||||||
colorizeSign="true"
|
|
||||||
isPercent="true"
|
|
||||||
position="end"
|
position="end"
|
||||||
|
[colorizeSign]="true"
|
||||||
|
[isPercent]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[value]="
|
[value]="
|
||||||
isLoading ? undefined : performance?.currentGrossPerformancePercent
|
isLoading ? undefined : performance?.currentGrossPerformancePercent
|
||||||
@ -39,17 +39,17 @@
|
|||||||
<div class="d-flex flex-column flex-wrap justify-content-end">
|
<div class="d-flex flex-column flex-wrap justify-content-end">
|
||||||
<gf-value
|
<gf-value
|
||||||
class="justify-content-end mb-2"
|
class="justify-content-end mb-2"
|
||||||
colorizeSign="true"
|
|
||||||
position="end"
|
position="end"
|
||||||
|
[colorizeSign]="true"
|
||||||
[currency]="baseCurrency"
|
[currency]="baseCurrency"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[value]="isLoading ? undefined : performance?.currentNetPerformance"
|
[value]="isLoading ? undefined : performance?.currentNetPerformance"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
<gf-value
|
<gf-value
|
||||||
class="justify-content-end"
|
class="justify-content-end"
|
||||||
colorizeSign="true"
|
|
||||||
isPercent="true"
|
|
||||||
position="end"
|
position="end"
|
||||||
|
[colorizeSign]="true"
|
||||||
|
[isPercent]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[value]="
|
[value]="
|
||||||
isLoading ? undefined : performance?.currentNetPerformancePercent
|
isLoading ? undefined : performance?.currentNetPerformancePercent
|
||||||
|
@ -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
|
||||||
|
@ -21,19 +21,22 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
<gf-value
|
<gf-value
|
||||||
colorizeSign="true"
|
label="Change"
|
||||||
isPercent="true"
|
|
||||||
label="Performance"
|
|
||||||
size="medium"
|
size="medium"
|
||||||
|
[colorizeSign]="true"
|
||||||
|
[currency]="data.baseCurrency"
|
||||||
[locale]="data.locale"
|
[locale]="data.locale"
|
||||||
[value]="grossPerformancePercent"
|
[value]="grossPerformance"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
<gf-value
|
<gf-value
|
||||||
label="First Buy Date"
|
label="Performance"
|
||||||
size="medium"
|
size="medium"
|
||||||
[value]="firstBuyDate"
|
[colorizeSign]="true"
|
||||||
|
[isPercent]="true"
|
||||||
|
[locale]="data.locale"
|
||||||
|
[value]="grossPerformancePercent"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
@ -76,9 +79,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
<gf-value
|
<gf-value
|
||||||
isCurrency="true"
|
|
||||||
label="Quantity"
|
label="Quantity"
|
||||||
size="medium"
|
size="medium"
|
||||||
|
[isCurrency]="true"
|
||||||
[value]="quantity"
|
[value]="quantity"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
@ -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>
|
||||||
|
@ -42,14 +42,14 @@
|
|||||||
<div class="d-flex mt-1">
|
<div class="d-flex mt-1">
|
||||||
<gf-value
|
<gf-value
|
||||||
class="mr-3"
|
class="mr-3"
|
||||||
colorizeSign="true"
|
[colorizeSign]="true"
|
||||||
[currency]="position?.currency"
|
[currency]="position?.currency"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[value]="position?.grossPerformance"
|
[value]="position?.grossPerformance"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
<gf-value
|
<gf-value
|
||||||
colorizeSign="true"
|
[colorizeSign]="true"
|
||||||
isPercent="true"
|
[isPercent]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[value]="position?.grossPerformancePercent"
|
[value]="position?.grossPerformancePercent"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
|
@ -28,8 +28,8 @@
|
|||||||
<td class="d-none d-lg-table-cell" mat-cell *matCellDef="let element">
|
<td class="d-none d-lg-table-cell" 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"
|
||||||
isPercent="true"
|
[isPercent]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[value]="isLoading ? undefined : element.grossPerformancePercent"
|
[value]="isLoading ? undefined : element.grossPerformancePercent"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
@ -50,7 +50,7 @@
|
|||||||
<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">
|
||||||
<gf-value
|
<gf-value
|
||||||
isPercent="true"
|
[isPercent]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[value]="isLoading ? undefined : element.shareInvestment"
|
[value]="isLoading ? undefined : element.shareInvestment"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
@ -71,7 +71,7 @@
|
|||||||
<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">
|
||||||
<gf-value
|
<gf-value
|
||||||
isPercent="true"
|
[isPercent]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[value]="isLoading ? undefined : element.shareCurrent"
|
[value]="isLoading ? undefined : element.shareCurrent"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
<div class="py-3">
|
<div class="py-3">
|
||||||
<div class="flex-nowrap no-gutters row">
|
<div class="align-items-center flex-nowrap no-gutters row">
|
||||||
<div *ngIf="isLoading">
|
<div *ngIf="isLoading">
|
||||||
<ngx-skeleton-loader
|
<ngx-skeleton-loader
|
||||||
animation="pulse"
|
animation="pulse"
|
||||||
class="mr-3"
|
class="mr-2"
|
||||||
[theme]="{
|
[theme]="{
|
||||||
height: '3rem',
|
height: '2rem',
|
||||||
width: '3rem'
|
width: '2rem'
|
||||||
}"
|
}"
|
||||||
></ngx-skeleton-loader>
|
></ngx-skeleton-loader>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
*ngIf="!isLoading"
|
*ngIf="!isLoading"
|
||||||
class="align-items-center d-flex icon-container mr-3 px-3"
|
class="align-items-center d-flex icon-container mr-2 px-2"
|
||||||
[ngClass]="{ okay: rule?.value === true, warn: rule?.value === false }"
|
[ngClass]="{ okay: rule?.value === true, warn: rule?.value === false }"
|
||||||
>
|
>
|
||||||
<ion-icon
|
<ion-icon
|
||||||
|
@ -2,15 +2,16 @@
|
|||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
.icon-container {
|
.icon-container {
|
||||||
|
background-color: rgba(var(--dark-primary-text), 0.05);
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
height: 3rem;
|
height: 2rem;
|
||||||
|
|
||||||
&.okay {
|
&.okay {
|
||||||
background-color: var(--success);
|
color: var(--success);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.warn {
|
&.warn {
|
||||||
background-color: var(--warning);
|
color: var(--danger);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,6 +22,6 @@
|
|||||||
|
|
||||||
:host-context(.is-dark-theme) {
|
:host-context(.is-dark-theme) {
|
||||||
.icon-container {
|
.icon-container {
|
||||||
color: rgba(var(--dark-primary-text));
|
background-color: rgba(var(--light-primary-text), 0.05);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,25 +17,21 @@
|
|||||||
mat-table
|
mat-table
|
||||||
[dataSource]="dataSource"
|
[dataSource]="dataSource"
|
||||||
>
|
>
|
||||||
<ng-container matColumnDef="platform">
|
<ng-container matColumnDef="account">
|
||||||
<th
|
<th *matHeaderCellDef i18n mat-header-cell mat-sort-header>Account</th>
|
||||||
*matHeaderCellDef
|
<td *matCellDef="let element" mat-cell>
|
||||||
class="d-none d-lg-table-cell text-center px-0"
|
<div class="d-flex">
|
||||||
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
|
<gf-symbol-icon
|
||||||
*ngIf="element.Platform?.url"
|
*ngIf="element.Account?.Platform?.url"
|
||||||
[tooltip]="element.Platform?.name"
|
class="mr-1"
|
||||||
[url]="element.Platform?.url"
|
[tooltip]="element.Account?.Platform?.name"
|
||||||
|
[url]="element.Account?.Platform?.url"
|
||||||
></gf-symbol-icon>
|
></gf-symbol-icon>
|
||||||
|
<span class="d-none d-lg-block">{{ element.Account?.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="date">
|
<ng-container matColumnDef="date">
|
||||||
<th
|
<th
|
||||||
*matHeaderCellDef
|
*matHeaderCellDef
|
||||||
@ -116,7 +112,7 @@
|
|||||||
<td *matCellDef="let element" class="d-none d-lg-table-cell" mat-cell>
|
<td *matCellDef="let element" class="d-none d-lg-table-cell" mat-cell>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<gf-value
|
<gf-value
|
||||||
isCurrency="true"
|
[isCurrency]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[value]="isLoading ? undefined : element.quantity"
|
[value]="isLoading ? undefined : element.quantity"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
@ -137,7 +133,7 @@
|
|||||||
<td *matCellDef="let element" class="d-none d-lg-table-cell" mat-cell>
|
<td *matCellDef="let element" class="d-none d-lg-table-cell" mat-cell>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<gf-value
|
<gf-value
|
||||||
isCurrency="true"
|
[isCurrency]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[value]="isLoading ? undefined : element.unitPrice"
|
[value]="isLoading ? undefined : element.unitPrice"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
@ -158,7 +154,7 @@
|
|||||||
<td class="d-none d-lg-table-cell" mat-cell *matCellDef="let element">
|
<td class="d-none d-lg-table-cell" mat-cell *matCellDef="let element">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<gf-value
|
<gf-value
|
||||||
isCurrency="true"
|
[isCurrency]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[value]="isLoading ? undefined : element.fee"
|
[value]="isLoading ? undefined : element.fee"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
|
@ -68,7 +68,7 @@ export class TransactionsTableComponent
|
|||||||
|
|
||||||
public ngOnChanges() {
|
public ngOnChanges() {
|
||||||
this.displayedColumns = [
|
this.displayedColumns = [
|
||||||
'platform',
|
'account',
|
||||||
'date',
|
'date',
|
||||||
'type',
|
'type',
|
||||||
'symbol',
|
'symbol',
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
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 } from '@ghostfolio/helper';
|
import { baseCurrency, hasPermission, permissions } from '@ghostfolio/helper';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
@ -15,6 +16,7 @@ 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;
|
||||||
@ -35,6 +37,15 @@ 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,7 +61,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-5 row">
|
<div *ngIf="hasPermissionForSubscription" class="mb-5 row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h3 class="mb-3 text-center" i18n>Pricing Plans</h3>
|
<h3 class="mb-3 text-center" i18n>Pricing Plans</h3>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -3,12 +3,15 @@ import { Access } from '@ghostfolio/api/app/access/interfaces/access.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 { DEFAULT_DATE_FORMAT } from '@ghostfolio/helper';
|
import {
|
||||||
|
DEFAULT_DATE_FORMAT,
|
||||||
|
hasPermission,
|
||||||
|
permissions
|
||||||
|
} 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',
|
||||||
@ -19,6 +22,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
|||||||
public baseCurrency: Currency;
|
public baseCurrency: Currency;
|
||||||
public currencies: Currency[] = [];
|
public currencies: Currency[] = [];
|
||||||
public defaultDateFormat = DEFAULT_DATE_FORMAT;
|
public defaultDateFormat = DEFAULT_DATE_FORMAT;
|
||||||
|
public hasPermissionForSubscription: boolean;
|
||||||
public user: User;
|
public user: User;
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
@ -34,8 +38,13 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
|||||||
this.dataService
|
this.dataService
|
||||||
.fetchInfo()
|
.fetchInfo()
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(({ currencies }) => {
|
.subscribe(({ currencies, globalPermissions }) => {
|
||||||
this.currencies = currencies;
|
this.currencies = currencies;
|
||||||
|
|
||||||
|
this.hasPermissionForSubscription = hasPermission(
|
||||||
|
globalPermissions,
|
||||||
|
permissions.enableSubscription
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.tokenStorageService
|
this.tokenStorageService
|
||||||
@ -70,6 +79,11 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
|
}
|
||||||
|
|
||||||
private update() {
|
private update() {
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchAccesses()
|
.fetchAccesses()
|
||||||
@ -80,9 +94,4 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
|||||||
this.cd.markForCheck();
|
this.cd.markForCheck();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
|
||||||
this.unsubscribeSubject.next();
|
|
||||||
this.unsubscribeSubject.complete();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
<div class="w-50" i18n>Alias</div>
|
<div class="w-50" i18n>Alias</div>
|
||||||
<div class="w-50">{{ user.alias }}</div>
|
<div class="w-50">{{ user.alias }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex py-1">
|
<div *ngIf="hasPermissionForSubscription" class="d-flex py-1">
|
||||||
<div class="w-50" i18n>Membership</div>
|
<div class="w-50" i18n>Membership</div>
|
||||||
<div class="w-50">
|
<div class="w-50">
|
||||||
<div class="align-items-center d-flex mb-1">
|
<div class="align-items-center d-flex mb-1">
|
||||||
|
@ -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 {}
|
215
apps/client/src/app/pages/accounts/accounts-page.component.ts
Normal file
215
apps/client/src/app/pages/accounts/accounts-page.component.ts
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.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 { Order as OrderModel } 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: OrderModel[];
|
||||||
|
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: OrderModel) {
|
||||||
|
this.router.navigate([], {
|
||||||
|
queryParams: { editDialog: true, transactionId: aAccount.id }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public openUpdateAccountDialog({
|
||||||
|
accountId,
|
||||||
|
currency,
|
||||||
|
dataSource,
|
||||||
|
date,
|
||||||
|
fee,
|
||||||
|
id,
|
||||||
|
platformId,
|
||||||
|
quantity,
|
||||||
|
symbol,
|
||||||
|
type,
|
||||||
|
unitPrice
|
||||||
|
}: OrderModel): void {
|
||||||
|
const dialogRef = this.dialog.open(CreateOrUpdateAccountDialog, {
|
||||||
|
data: {
|
||||||
|
accounts: this.user.accounts,
|
||||||
|
transaction: {
|
||||||
|
accountId,
|
||||||
|
currency,
|
||||||
|
dataSource,
|
||||||
|
date,
|
||||||
|
fee,
|
||||||
|
id,
|
||||||
|
platformId,
|
||||||
|
quantity,
|
||||||
|
symbol,
|
||||||
|
type,
|
||||||
|
unitPrice
|
||||||
|
}
|
||||||
|
},
|
||||||
|
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||||
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef.afterClosed().subscribe((data: any) => {
|
||||||
|
const transaction: UpdateOrderDto = data?.transaction;
|
||||||
|
|
||||||
|
if (transaction) {
|
||||||
|
this.dataService.putAccount(transaction).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: {
|
||||||
|
accounts: this.user?.accounts,
|
||||||
|
transaction: {
|
||||||
|
accountId: this.user?.accounts.find((account) => {
|
||||||
|
return account.isDefault;
|
||||||
|
})?.id,
|
||||||
|
currency: null,
|
||||||
|
date: new Date(),
|
||||||
|
fee: 0,
|
||||||
|
platformId: null,
|
||||||
|
quantity: null,
|
||||||
|
symbol: null,
|
||||||
|
type: 'BUY',
|
||||||
|
unitPrice: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||||
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef.afterClosed().subscribe((data: any) => {
|
||||||
|
const transaction: UpdateOrderDto = data?.transaction;
|
||||||
|
|
||||||
|
if (transaction) {
|
||||||
|
this.dataService.postAccount(transaction).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,104 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
Inject
|
||||||
|
} from '@angular/core';
|
||||||
|
import { FormControl, Validators } from '@angular/forms';
|
||||||
|
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
||||||
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
|
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||||
|
import { Currency } from '@prisma/client';
|
||||||
|
import { Observable, Subject } from 'rxjs';
|
||||||
|
import {
|
||||||
|
debounceTime,
|
||||||
|
distinctUntilChanged,
|
||||||
|
startWith,
|
||||||
|
switchMap,
|
||||||
|
takeUntil
|
||||||
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { DataService } from '../../../services/data.service';
|
||||||
|
import { CreateOrUpdateAccountDialogParams } from './interfaces/interfaces';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
host: { class: 'h-100' },
|
||||||
|
selector: '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 filteredLookupItems: Observable<LookupItem[]>;
|
||||||
|
public isLoading = false;
|
||||||
|
public platforms: { id: string; name: string }[];
|
||||||
|
public searchSymbolCtrl = new FormControl(
|
||||||
|
this.data.transaction.symbol,
|
||||||
|
Validators.required
|
||||||
|
);
|
||||||
|
|
||||||
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private cd: ChangeDetectorRef,
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.filteredLookupItems = this.searchSymbolCtrl.valueChanges.pipe(
|
||||||
|
startWith(''),
|
||||||
|
debounceTime(400),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
switchMap((aQuery: string) => {
|
||||||
|
if (aQuery) {
|
||||||
|
return this.dataService.fetchSymbols(aQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public onCancel(): void {
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public onUpdateSymbol(event: MatAutocompleteSelectedEvent) {
|
||||||
|
this.isLoading = true;
|
||||||
|
this.data.transaction.symbol = event.option.value;
|
||||||
|
|
||||||
|
this.dataService
|
||||||
|
.fetchSymbolItem(this.data.transaction.symbol)
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(({ currency, dataSource, marketPrice }) => {
|
||||||
|
this.data.transaction.currency = currency;
|
||||||
|
this.data.transaction.dataSource = dataSource;
|
||||||
|
this.data.transaction.unitPrice = marketPrice;
|
||||||
|
|
||||||
|
this.isLoading = false;
|
||||||
|
|
||||||
|
this.cd.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public onUpdateSymbolByTyping(value: string) {
|
||||||
|
this.data.transaction.currency = null;
|
||||||
|
this.data.transaction.dataSource = null;
|
||||||
|
this.data.transaction.unitPrice = null;
|
||||||
|
|
||||||
|
this.data.transaction.symbol = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,163 @@
|
|||||||
|
<form #addAccountForm="ngForm" class="d-flex flex-column h-100">
|
||||||
|
<h1 *ngIf="data.transaction.id" mat-dialog-title i18n>Update account</h1>
|
||||||
|
<h1 *ngIf="!data.transaction.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>Symbol or ISIN</mat-label>
|
||||||
|
<input
|
||||||
|
autocapitalize="off"
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
matInput
|
||||||
|
required
|
||||||
|
[formControl]="searchSymbolCtrl"
|
||||||
|
[matAutocomplete]="auto"
|
||||||
|
(change)="onUpdateSymbolByTyping($event.target.value)"
|
||||||
|
/>
|
||||||
|
<mat-autocomplete
|
||||||
|
#auto="matAutocomplete"
|
||||||
|
(optionSelected)="onUpdateSymbol($event)"
|
||||||
|
>
|
||||||
|
<ng-container>
|
||||||
|
<mat-option
|
||||||
|
*ngFor="let lookupItem of filteredLookupItems | async"
|
||||||
|
class="autocomplete"
|
||||||
|
[value]="lookupItem.symbol"
|
||||||
|
>
|
||||||
|
<span class="mr-2 symbol">{{ lookupItem.symbol | gfSymbol }}</span
|
||||||
|
><span><b>{{ lookupItem.name }}</b></span>
|
||||||
|
</mat-option>
|
||||||
|
</ng-container>
|
||||||
|
</mat-autocomplete>
|
||||||
|
<mat-spinner *ngIf="isLoading" matSuffix [diameter]="20"></mat-spinner>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label i18n>Type</mat-label>
|
||||||
|
<mat-select name="type" required [(value)]="data.transaction.type">
|
||||||
|
<mat-option value="BUY" i18n> BUY </mat-option>
|
||||||
|
<mat-option value="SELL" i18n> SELL </mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label i18n>Currency</mat-label>
|
||||||
|
<mat-select
|
||||||
|
class="no-arrow"
|
||||||
|
disabled
|
||||||
|
name="currency"
|
||||||
|
required
|
||||||
|
[(value)]="data.transaction.currency"
|
||||||
|
>
|
||||||
|
<mat-option *ngFor="let currency of currencies" [value]="currency"
|
||||||
|
>{{ currency }}</mat-option
|
||||||
|
>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div class="d-none">
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label i18n>Data Source</mat-label>
|
||||||
|
<input
|
||||||
|
disabled
|
||||||
|
matInput
|
||||||
|
name="dataSource"
|
||||||
|
required
|
||||||
|
[(ngModel)]="data.transaction.dataSource"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label i18n>Date</mat-label>
|
||||||
|
<input
|
||||||
|
disabled
|
||||||
|
matInput
|
||||||
|
name="date"
|
||||||
|
required
|
||||||
|
[matDatepicker]="date"
|
||||||
|
[(ngModel)]="data.transaction.date"
|
||||||
|
/>
|
||||||
|
<mat-datepicker-toggle matSuffix [for]="date"></mat-datepicker-toggle>
|
||||||
|
<mat-datepicker #date disabled="false"></mat-datepicker>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label i18n>Fee</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
name="fee"
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
[(ngModel)]="data.transaction.fee"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label i18n>Quantity</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
name="quantity"
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
[(ngModel)]="data.transaction.quantity"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label i18n>Unit Price</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
name="unitPrice"
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
[(ngModel)]="data.transaction.unitPrice"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div class="d-none">
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label i18n>Account</mat-label>
|
||||||
|
<mat-select
|
||||||
|
disabled
|
||||||
|
name="accountId"
|
||||||
|
required
|
||||||
|
[(value)]="data.transaction.accountId"
|
||||||
|
>
|
||||||
|
<mat-option *ngFor="let account of data.accounts" [value]="account.id"
|
||||||
|
>{{ account.name }}</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.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 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 && data.transaction.symbol)"
|
||||||
|
[mat-dialog-close]="data"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
@ -0,0 +1,35 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||||
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
|
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||||
|
|
||||||
|
import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [CreateOrUpdateAccountDialog],
|
||||||
|
exports: [],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
GfSymbolModule,
|
||||||
|
FormsModule,
|
||||||
|
MatAutocompleteModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatDatepickerModule,
|
||||||
|
MatDialogModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatProgressSpinnerModule,
|
||||||
|
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,9 @@
|
|||||||
|
import { Account } from '@prisma/client';
|
||||||
|
|
||||||
|
import { Order } from '../../interfaces/order.interface';
|
||||||
|
|
||||||
|
export interface CreateOrUpdateAccountDialogParams {
|
||||||
|
accountId: string;
|
||||||
|
accounts: Account[];
|
||||||
|
transaction: Order;
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
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;
|
||||||
|
}
|
@ -70,42 +70,36 @@
|
|||||||
<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">
|
<mat-card class="px-0">
|
||||||
<mat-card-content>
|
<mat-card-content class="users">
|
||||||
<table class="users w-100">
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="mat-header-row">
|
<tr class="mat-header-row">
|
||||||
<th class="mat-header-cell pl-2 py-2 text-truncate" i18n>
|
<th class="mat-header-cell pl-3 py-2" i18n>User</th>
|
||||||
User
|
<th class="mat-header-cell pr-2 py-2" i18n>
|
||||||
</th>
|
|
||||||
<th class="mat-header-cell pr-2 py-2 text-truncate" i18n>
|
|
||||||
Registration Date
|
Registration Date
|
||||||
</th>
|
</th>
|
||||||
<th class="mat-header-cell pr-2 py-2 text-truncate" i18n>
|
<th class="mat-header-cell pr-2 py-2" i18n>Accounts</th>
|
||||||
Transactions
|
<th class="mat-header-cell pr-2 py-2" i18n>Transactions</th>
|
||||||
</th>
|
<th class="mat-header-cell pr-2 py-2" i18n>Engagement</th>
|
||||||
<th class="mat-header-cell pr-2 py-2 text-truncate" i18n>
|
<th class="mat-header-cell pr-3 py-2" i18n>Last Activitiy</th>
|
||||||
Engagement
|
|
||||||
</th>
|
|
||||||
<th class="mat-header-cell pr-2 py-2 text-truncate" i18n>
|
|
||||||
Last Activitiy
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let userItem of users" class="mat-row">
|
<tr *ngFor="let userItem of users" class="mat-row">
|
||||||
<td class="mat-cell pl-2 py-2 text-truncate">
|
<td class="mat-cell pl-3 py-2">
|
||||||
{{ userItem.alias || userItem.id }}
|
{{ userItem.alias || userItem.id }}
|
||||||
</td>
|
</td>
|
||||||
<td class="mat-cell pr-2 py-2 text-truncate">
|
<td class="mat-cell pr-2 py-2">
|
||||||
{{ userItem.createdAt | date: defaultDateFormat }}
|
{{ userItem.createdAt | date: defaultDateFormat }}
|
||||||
</td>
|
</td>
|
||||||
<td class="mat-cell pr-2 py-2 text-truncate">
|
<td class="mat-cell pr-2 py-2">
|
||||||
{{ userItem._count?.Order }}
|
{{ userItem._count?.Account }}
|
||||||
</td>
|
</td>
|
||||||
<td class="mat-cell pr-2 py-2 text-truncate">
|
<td class="mat-cell pr-2 py-2">{{ userItem._count?.Order }}</td>
|
||||||
|
<td class="mat-cell pr-2 py-2">
|
||||||
{{ userItem.Analytics?.activityCount }}
|
{{ userItem.Analytics?.activityCount }}
|
||||||
</td>
|
</td>
|
||||||
<td class="mat-cell pr-2 py-2 text-truncate">
|
<td class="mat-cell pr-3 py-2">
|
||||||
{{ formatDistanceToNow(userItem.Analytics?.updatedAt) }}
|
{{ formatDistanceToNow(userItem.Analytics?.updatedAt) }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -2,16 +2,25 @@
|
|||||||
color: rgb(var(--dark-primary-text));
|
color: rgb(var(--dark-primary-text));
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
table {
|
.mat-card-content {
|
||||||
&.users {
|
&.users {
|
||||||
table-layout: fixed;
|
overflow-x: auto;
|
||||||
|
|
||||||
tr {
|
table {
|
||||||
&:nth-child(even) {
|
min-width: 100%;
|
||||||
background-color: rgba(
|
|
||||||
var(--dark-primary-text),
|
tr {
|
||||||
var(--palette-background-hover-alpha)
|
&:nth-child(even) {
|
||||||
);
|
background-color: rgba(
|
||||||
|
var(--dark-primary-text),
|
||||||
|
var(--palette-background-hover-alpha)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-row,
|
||||||
|
.mat-header-row {
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import { FormControl, Validators } from '@angular/forms';
|
|||||||
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
|
||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||||
import { Currency, Order as OrderModel } from '@prisma/client';
|
import { Currency } from '@prisma/client';
|
||||||
import { Observable, Subject } from 'rxjs';
|
import { Observable, Subject } from 'rxjs';
|
||||||
import {
|
import {
|
||||||
debounceTime,
|
debounceTime,
|
||||||
@ -19,6 +19,7 @@ import {
|
|||||||
} from 'rxjs/operators';
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
import { DataService } from '../../../services/data.service';
|
import { DataService } from '../../../services/data.service';
|
||||||
|
import { CreateOrUpdateTransactionDialogParams } from './interfaces/interfaces';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'h-100' },
|
host: { class: 'h-100' },
|
||||||
@ -33,7 +34,7 @@ export class CreateOrUpdateTransactionDialog {
|
|||||||
public isLoading = false;
|
public isLoading = false;
|
||||||
public platforms: { id: string; name: string }[];
|
public platforms: { id: string; name: string }[];
|
||||||
public searchSymbolCtrl = new FormControl(
|
public searchSymbolCtrl = new FormControl(
|
||||||
this.data.symbol,
|
this.data.transaction.symbol,
|
||||||
Validators.required
|
Validators.required
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -43,7 +44,7 @@ export class CreateOrUpdateTransactionDialog {
|
|||||||
private cd: ChangeDetectorRef,
|
private cd: ChangeDetectorRef,
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
public dialogRef: MatDialogRef<CreateOrUpdateTransactionDialog>,
|
public dialogRef: MatDialogRef<CreateOrUpdateTransactionDialog>,
|
||||||
@Inject(MAT_DIALOG_DATA) public data: OrderModel
|
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateTransactionDialogParams
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
@ -72,14 +73,15 @@ export class CreateOrUpdateTransactionDialog {
|
|||||||
|
|
||||||
public onUpdateSymbol(event: MatAutocompleteSelectedEvent) {
|
public onUpdateSymbol(event: MatAutocompleteSelectedEvent) {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.data.symbol = event.option.value;
|
this.data.transaction.symbol = event.option.value;
|
||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchSymbolItem(this.data.symbol)
|
.fetchSymbolItem(this.data.transaction.symbol)
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(({ currency, marketPrice }) => {
|
.subscribe(({ currency, dataSource, marketPrice }) => {
|
||||||
this.data.currency = currency;
|
this.data.transaction.currency = currency;
|
||||||
this.data.unitPrice = marketPrice;
|
this.data.transaction.dataSource = dataSource;
|
||||||
|
this.data.transaction.unitPrice = marketPrice;
|
||||||
|
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
|
|
||||||
@ -88,10 +90,11 @@ export class CreateOrUpdateTransactionDialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onUpdateSymbolByTyping(value: string) {
|
public onUpdateSymbolByTyping(value: string) {
|
||||||
this.data.currency = null;
|
this.data.transaction.currency = null;
|
||||||
this.data.unitPrice = null;
|
this.data.transaction.dataSource = null;
|
||||||
|
this.data.transaction.unitPrice = null;
|
||||||
|
|
||||||
this.data.symbol = value;
|
this.data.transaction.symbol = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<form #addTransactionForm="ngForm" class="d-flex flex-column h-100">
|
<form #addTransactionForm="ngForm" class="d-flex flex-column h-100">
|
||||||
<h1 *ngIf="data.id" mat-dialog-title i18n>Update transaction</h1>
|
<h1 *ngIf="data.transaction.id" mat-dialog-title i18n>Update transaction</h1>
|
||||||
<h1 *ngIf="!data.id" mat-dialog-title i18n>Add transaction</h1>
|
<h1 *ngIf="!data.transaction.id" mat-dialog-title i18n>Add transaction</h1>
|
||||||
<div class="flex-grow-1" mat-dialog-content>
|
<div class="flex-grow-1" mat-dialog-content>
|
||||||
<div>
|
<div>
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
@ -25,7 +25,7 @@
|
|||||||
class="autocomplete"
|
class="autocomplete"
|
||||||
[value]="lookupItem.symbol"
|
[value]="lookupItem.symbol"
|
||||||
>
|
>
|
||||||
<span class="mr-2 symbol">{{ lookupItem.symbol }}</span
|
<span class="mr-2 symbol">{{ lookupItem.symbol | gfSymbol }}</span
|
||||||
><span><b>{{ lookupItem.name }}</b></span>
|
><span><b>{{ lookupItem.name }}</b></span>
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@ -36,7 +36,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
<mat-label i18n>Type</mat-label>
|
<mat-label i18n>Type</mat-label>
|
||||||
<mat-select name="type" required [(value)]="data.type">
|
<mat-select name="type" required [(value)]="data.transaction.type">
|
||||||
<mat-option value="BUY" i18n> BUY </mat-option>
|
<mat-option value="BUY" i18n> BUY </mat-option>
|
||||||
<mat-option value="SELL" i18n> SELL </mat-option>
|
<mat-option value="SELL" i18n> SELL </mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
@ -50,7 +50,7 @@
|
|||||||
disabled
|
disabled
|
||||||
name="currency"
|
name="currency"
|
||||||
required
|
required
|
||||||
[(value)]="data.currency"
|
[(value)]="data.transaction.currency"
|
||||||
>
|
>
|
||||||
<mat-option *ngFor="let currency of currencies" [value]="currency"
|
<mat-option *ngFor="let currency of currencies" [value]="currency"
|
||||||
>{{ currency }}</mat-option
|
>{{ currency }}</mat-option
|
||||||
@ -58,6 +58,18 @@
|
|||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="d-none">
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label i18n>Data Source</mat-label>
|
||||||
|
<input
|
||||||
|
disabled
|
||||||
|
matInput
|
||||||
|
name="dataSource"
|
||||||
|
required
|
||||||
|
[(ngModel)]="data.transaction.dataSource"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
<mat-label i18n>Date</mat-label>
|
<mat-label i18n>Date</mat-label>
|
||||||
@ -67,7 +79,7 @@
|
|||||||
name="date"
|
name="date"
|
||||||
required
|
required
|
||||||
[matDatepicker]="date"
|
[matDatepicker]="date"
|
||||||
[(ngModel)]="data.date"
|
[(ngModel)]="data.transaction.date"
|
||||||
/>
|
/>
|
||||||
<mat-datepicker-toggle matSuffix [for]="date"></mat-datepicker-toggle>
|
<mat-datepicker-toggle matSuffix [for]="date"></mat-datepicker-toggle>
|
||||||
<mat-datepicker #date disabled="false"></mat-datepicker>
|
<mat-datepicker #date disabled="false"></mat-datepicker>
|
||||||
@ -81,7 +93,7 @@
|
|||||||
name="fee"
|
name="fee"
|
||||||
required
|
required
|
||||||
type="number"
|
type="number"
|
||||||
[(ngModel)]="data.fee"
|
[(ngModel)]="data.transaction.fee"
|
||||||
/>
|
/>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
@ -93,7 +105,7 @@
|
|||||||
name="quantity"
|
name="quantity"
|
||||||
required
|
required
|
||||||
type="number"
|
type="number"
|
||||||
[(ngModel)]="data.quantity"
|
[(ngModel)]="data.transaction.quantity"
|
||||||
/>
|
/>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
@ -105,14 +117,29 @@
|
|||||||
name="unitPrice"
|
name="unitPrice"
|
||||||
required
|
required
|
||||||
type="number"
|
type="number"
|
||||||
[(ngModel)]="data.unitPrice"
|
[(ngModel)]="data.transaction.unitPrice"
|
||||||
/>
|
/>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="d-none">
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label i18n>Account</mat-label>
|
||||||
|
<mat-select
|
||||||
|
disabled
|
||||||
|
name="accountId"
|
||||||
|
required
|
||||||
|
[(value)]="data.transaction.accountId"
|
||||||
|
>
|
||||||
|
<mat-option *ngFor="let account of data.accounts" [value]="account.id"
|
||||||
|
>{{ account.name }}</mat-option
|
||||||
|
>
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
<mat-label i18n>Platform</mat-label>
|
<mat-label i18n>Platform</mat-label>
|
||||||
<mat-select name="platformId" [(value)]="data.platformId">
|
<mat-select name="platformId" [(value)]="data.transaction.platformId">
|
||||||
<mat-option [value]="null"></mat-option>
|
<mat-option [value]="null"></mat-option>
|
||||||
<mat-option *ngFor="let platform of platforms" [value]="platform.id"
|
<mat-option *ngFor="let platform of platforms" [value]="platform.id"
|
||||||
>{{ platform.name }}</mat-option
|
>{{ platform.name }}</mat-option
|
||||||
@ -127,7 +154,7 @@
|
|||||||
color="primary"
|
color="primary"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[disabled]="!(addTransactionForm.form.valid && data.symbol)"
|
[disabled]="!(addTransactionForm.form.valid && data.transaction.symbol)"
|
||||||
[mat-dialog-close]="data"
|
[mat-dialog-close]="data"
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
|
@ -9,6 +9,7 @@ import { MatFormFieldModule } from '@angular/material/form-field';
|
|||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
|
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||||
|
|
||||||
import { CreateOrUpdateTransactionDialog } from './create-or-update-transaction-dialog.component';
|
import { CreateOrUpdateTransactionDialog } from './create-or-update-transaction-dialog.component';
|
||||||
|
|
||||||
@ -17,6 +18,7 @@ import { CreateOrUpdateTransactionDialog } from './create-or-update-transaction-
|
|||||||
exports: [],
|
exports: [],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
GfSymbolModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
MatAutocompleteModule,
|
MatAutocompleteModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
import { Account } from '@prisma/client';
|
||||||
|
|
||||||
|
import { Order } from '../../interfaces/order.interface';
|
||||||
|
|
||||||
|
export interface CreateOrUpdateTransactionDialogParams {
|
||||||
|
accountId: string;
|
||||||
|
accounts: Account[];
|
||||||
|
transaction: Order;
|
||||||
|
}
|
@ -1,8 +1,14 @@
|
|||||||
|
import { Currency, DataSource } from '@prisma/client';
|
||||||
|
|
||||||
export interface Order {
|
export interface Order {
|
||||||
currency: string;
|
accountId: string;
|
||||||
|
currency: Currency;
|
||||||
|
dataSource: DataSource;
|
||||||
date: Date;
|
date: Date;
|
||||||
fee: number;
|
fee: number;
|
||||||
|
id: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
platformId: string;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
type: string;
|
type: string;
|
||||||
unitPrice: number;
|
unitPrice: number;
|
||||||
|
@ -122,37 +122,10 @@ export class TransactionsPageComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private openCreateTransactionDialog(): void {
|
|
||||||
const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, {
|
|
||||||
data: {
|
|
||||||
currency: null,
|
|
||||||
date: new Date(),
|
|
||||||
fee: 0,
|
|
||||||
platformId: null,
|
|
||||||
quantity: null,
|
|
||||||
symbol: null,
|
|
||||||
type: 'BUY',
|
|
||||||
unitPrice: null
|
|
||||||
},
|
|
||||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
|
||||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
|
||||||
});
|
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe((order: UpdateOrderDto) => {
|
|
||||||
if (order) {
|
|
||||||
this.dataService.postOrder(order).subscribe({
|
|
||||||
next: () => {
|
|
||||||
this.fetchOrders();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.router.navigate(['.'], { relativeTo: this.route });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public openUpdateTransactionDialog({
|
public openUpdateTransactionDialog({
|
||||||
|
accountId,
|
||||||
currency,
|
currency,
|
||||||
|
dataSource,
|
||||||
date,
|
date,
|
||||||
fee,
|
fee,
|
||||||
id,
|
id,
|
||||||
@ -164,23 +137,30 @@ export class TransactionsPageComponent implements OnInit {
|
|||||||
}: OrderModel): void {
|
}: OrderModel): void {
|
||||||
const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, {
|
const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, {
|
||||||
data: {
|
data: {
|
||||||
currency,
|
accounts: this.user.accounts,
|
||||||
date,
|
transaction: {
|
||||||
fee,
|
accountId,
|
||||||
id,
|
currency,
|
||||||
platformId,
|
dataSource,
|
||||||
quantity,
|
date,
|
||||||
symbol,
|
fee,
|
||||||
type,
|
id,
|
||||||
unitPrice
|
platformId,
|
||||||
|
quantity,
|
||||||
|
symbol,
|
||||||
|
type,
|
||||||
|
unitPrice
|
||||||
|
}
|
||||||
},
|
},
|
||||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||||
});
|
});
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe((order: UpdateOrderDto) => {
|
dialogRef.afterClosed().subscribe((data: any) => {
|
||||||
if (order) {
|
const transaction: UpdateOrderDto = data?.transaction;
|
||||||
this.dataService.putOrder(order).subscribe({
|
|
||||||
|
if (transaction) {
|
||||||
|
this.dataService.putOrder(transaction).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.fetchOrders();
|
this.fetchOrders();
|
||||||
}
|
}
|
||||||
@ -195,4 +175,41 @@ export class TransactionsPageComponent implements OnInit {
|
|||||||
this.unsubscribeSubject.next();
|
this.unsubscribeSubject.next();
|
||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private openCreateTransactionDialog(): void {
|
||||||
|
const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, {
|
||||||
|
data: {
|
||||||
|
accounts: this.user?.accounts,
|
||||||
|
transaction: {
|
||||||
|
accountId: this.user?.accounts.find((account) => {
|
||||||
|
return account.isDefault;
|
||||||
|
})?.id,
|
||||||
|
currency: null,
|
||||||
|
date: new Date(),
|
||||||
|
fee: 0,
|
||||||
|
platformId: null,
|
||||||
|
quantity: null,
|
||||||
|
symbol: null,
|
||||||
|
type: 'BUY',
|
||||||
|
unitPrice: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||||
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef.afterClosed().subscribe((data: any) => {
|
||||||
|
const transaction: UpdateOrderDto = data?.transaction;
|
||||||
|
|
||||||
|
if (transaction) {
|
||||||
|
this.dataService.postOrder(transaction).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.fetchOrders();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.router.navigate(['.'], { relativeTo: this.route });
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
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 { 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 { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
||||||
@ -28,10 +29,18 @@ export class DataService {
|
|||||||
|
|
||||||
public constructor(private http: HttpClient) {}
|
public constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
public fetchAccounts() {
|
||||||
|
return this.http.get<OrderModel[]>('/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}`);
|
||||||
}
|
}
|
||||||
@ -108,6 +117,10 @@ export class DataService {
|
|||||||
return this.http.get<any>(`/api/auth/anonymous/${accessToken}`);
|
return this.http.get<any>(`/api/auth/anonymous/${accessToken}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public postAccount(aAccount: UpdateAccountDto) {
|
||||||
|
return this.http.post<OrderModel>(`/api/account`, aAccount);
|
||||||
|
}
|
||||||
|
|
||||||
public postOrder(aOrder: UpdateOrderDto) {
|
public postOrder(aOrder: UpdateOrderDto) {
|
||||||
return this.http.post<OrderModel>(`/api/order`, aOrder);
|
return this.http.post<OrderModel>(`/api/order`, aOrder);
|
||||||
}
|
}
|
||||||
@ -116,6 +129,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);
|
||||||
}
|
}
|
||||||
|
@ -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": [
|
||||||
{
|
{
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>Ghostfolio – Privacy-first Portfolio Tracker</title>
|
<title>Ghostfolio – Open Source Portfolio Tracker</title>
|
||||||
<base href="/" />
|
<base href="/" />
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta content="yes" name="apple-mobile-web-app-capable" />
|
<meta content="yes" name="apple-mobile-web-app-capable" />
|
||||||
@ -25,13 +25,13 @@
|
|||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
name="twitter:title"
|
name="twitter:title"
|
||||||
content="Ghostfolio – Privacy-first Portfolio Tracker"
|
content="Ghostfolio – Open Source Portfolio Tracker"
|
||||||
/>
|
/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta property="og:description" content="" />
|
<meta property="og:description" content="" />
|
||||||
<meta
|
<meta
|
||||||
property="og:title"
|
property="og:title"
|
||||||
content="Ghostfolio – Privacy-first Portfolio Tracker"
|
content="Ghostfolio – Open Source Portfolio Tracker"
|
||||||
/>
|
/>
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:url" content="https://www.ghostfol.io" />
|
<meta property="og:url" content="https://www.ghostfol.io" />
|
||||||
@ -42,7 +42,7 @@
|
|||||||
<meta property="og:updated_time" content="2021-03-20T00:00:00+00:00" />
|
<meta property="og:updated_time" content="2021-03-20T00:00:00+00:00" />
|
||||||
<meta
|
<meta
|
||||||
property="og:site_name"
|
property="og:site_name"
|
||||||
content="Ghostfolio – Privacy-first Portfolio Tracker"
|
content="Ghostfolio – Open Source Portfolio Tracker"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<link
|
<link
|
||||||
|
@ -57,11 +57,20 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mat-card:not([class*='mat-elevation-z']) {
|
.mat-card {
|
||||||
border-color: rgba(
|
background: var(--dark-background);
|
||||||
var(--light-primary-text),
|
|
||||||
var(--palette-foreground-divider-alpha)
|
&:not([class*='mat-elevation-z']) {
|
||||||
);
|
border-color: rgba(
|
||||||
|
var(--light-primary-text),
|
||||||
|
var(--palette-foreground-divider-alpha)
|
||||||
|
);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-dialog-container {
|
||||||
|
background: var(--dark-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mat-fab,
|
.mat-fab,
|
||||||
|
@ -12,5 +12,9 @@
|
|||||||
{
|
{
|
||||||
"path": "./tsconfig.editor.json"
|
"path": "./tsconfig.editor.json"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"strictInjectionParameters": true,
|
||||||
|
"strictTemplates": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,11 +9,15 @@ export const permissions = {
|
|||||||
accessFearAndGreedIndex: 'accessFearAndGreedIndex',
|
accessFearAndGreedIndex: 'accessFearAndGreedIndex',
|
||||||
createAccount: 'createAccount',
|
createAccount: 'createAccount',
|
||||||
createOrder: 'createOrder',
|
createOrder: 'createOrder',
|
||||||
|
createUserAccount: 'createUserAccount',
|
||||||
|
deleteAccount: 'deleteAcccount',
|
||||||
deleteOrder: 'deleteOrder',
|
deleteOrder: 'deleteOrder',
|
||||||
|
enableSocialLogin: 'enableSocialLogin',
|
||||||
|
enableSubscription: 'enableSubscription',
|
||||||
readForeignPortfolio: 'readForeignPortfolio',
|
readForeignPortfolio: 'readForeignPortfolio',
|
||||||
|
updateAccount: 'updateAccount',
|
||||||
updateOrder: 'updateOrder',
|
updateOrder: 'updateOrder',
|
||||||
updateUserSettings: 'updateUserSettings',
|
updateUserSettings: 'updateUserSettings'
|
||||||
useSocialLogin: 'useSocialLogin'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function hasPermission(
|
export function hasPermission(
|
||||||
@ -36,7 +40,7 @@ export function getPermissions(aRole: Role): string[] {
|
|||||||
];
|
];
|
||||||
|
|
||||||
case 'DEMO':
|
case 'DEMO':
|
||||||
return [permissions.createAccount];
|
return [permissions.createUserAccount];
|
||||||
|
|
||||||
case 'USER':
|
case 'USER':
|
||||||
return [
|
return [
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ghostfolio",
|
"name": "ghostfolio",
|
||||||
"version": "0.90.0",
|
"version": "0.97.0",
|
||||||
"homepage": "https://ghostfol.io",
|
"homepage": "https://ghostfol.io",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -15,6 +15,9 @@
|
|||||||
"angular": "node --max_old_space_size=32768 ./node_modules/@angular/cli/bin/ng",
|
"angular": "node --max_old_space_size=32768 ./node_modules/@angular/cli/bin/ng",
|
||||||
"build:all": "ng build --prod api && ng build --prod client && yarn replace-placeholders-in-build",
|
"build:all": "ng build --prod api && ng build --prod client && yarn replace-placeholders-in-build",
|
||||||
"clean": "rimraf dist",
|
"clean": "rimraf dist",
|
||||||
|
"database:format-schema": "prisma format",
|
||||||
|
"database:generate-typings": "prisma generate",
|
||||||
|
"database:gui": "prisma studio",
|
||||||
"database:push": "prisma db push --preview-feature",
|
"database:push": "prisma db push --preview-feature",
|
||||||
"database:seed": "prisma db seed --preview-feature",
|
"database:seed": "prisma db seed --preview-feature",
|
||||||
"dep-graph": "nx dep-graph",
|
"dep-graph": "nx dep-graph",
|
||||||
|
@ -24,6 +24,22 @@ model Access {
|
|||||||
@@id([id, userId])
|
@@id([id, userId])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Account {
|
||||||
|
accountType AccountType @default(SECURITIES)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
id String @default(uuid())
|
||||||
|
isDefault Boolean @default(false)
|
||||||
|
name String?
|
||||||
|
Order Order[]
|
||||||
|
Platform Platform? @relation(fields: [platformId], references: [id])
|
||||||
|
platformId String?
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
User User @relation(fields: [userId], references: [id])
|
||||||
|
userId String
|
||||||
|
|
||||||
|
@@id([id, userId])
|
||||||
|
}
|
||||||
|
|
||||||
model Analytics {
|
model Analytics {
|
||||||
activityCount Int @default(0)
|
activityCount Int @default(0)
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@ -43,29 +59,34 @@ model MarketData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Order {
|
model Order {
|
||||||
createdAt DateTime @default(now())
|
Account Account? @relation(fields: [accountId, accountUserId], references: [id, userId])
|
||||||
currency Currency
|
accountId String?
|
||||||
date DateTime
|
accountUserId String?
|
||||||
fee Float
|
createdAt DateTime @default(now())
|
||||||
id String @default(uuid())
|
currency Currency
|
||||||
Platform Platform? @relation(fields: [platformId], references: [id])
|
dataSource DataSource @default(YAHOO)
|
||||||
platformId String?
|
date DateTime
|
||||||
quantity Float
|
fee Float
|
||||||
symbol String
|
id String @default(uuid())
|
||||||
type Type
|
Platform Platform? @relation(fields: [platformId], references: [id])
|
||||||
unitPrice Float
|
platformId String?
|
||||||
updatedAt DateTime @updatedAt
|
quantity Float
|
||||||
User User @relation(fields: [userId], references: [id])
|
symbol String
|
||||||
userId String
|
type Type
|
||||||
|
unitPrice Float
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
User User @relation(fields: [userId], references: [id])
|
||||||
|
userId String
|
||||||
|
|
||||||
@@id([id, userId])
|
@@id([id, userId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Platform {
|
model Platform {
|
||||||
id String @id @default(uuid())
|
Account Account[]
|
||||||
name String?
|
id String @id @default(uuid())
|
||||||
url String @unique
|
name String?
|
||||||
Order Order[]
|
Order Order[]
|
||||||
|
url String @unique
|
||||||
}
|
}
|
||||||
|
|
||||||
model Property {
|
model Property {
|
||||||
@ -84,6 +105,7 @@ model User {
|
|||||||
Access Access[] @relation("accessGet")
|
Access Access[] @relation("accessGet")
|
||||||
AccessGive Access[] @relation(name: "accessGive")
|
AccessGive Access[] @relation(name: "accessGive")
|
||||||
accessToken String?
|
accessToken String?
|
||||||
|
Account Account[]
|
||||||
alias String?
|
alias String?
|
||||||
Analytics Analytics?
|
Analytics Analytics?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
@ -96,6 +118,10 @@ model User {
|
|||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum AccountType {
|
||||||
|
SECURITIES
|
||||||
|
}
|
||||||
|
|
||||||
enum Currency {
|
enum Currency {
|
||||||
CHF
|
CHF
|
||||||
EUR
|
EUR
|
||||||
@ -103,6 +129,12 @@ enum Currency {
|
|||||||
USD
|
USD
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum DataSource {
|
||||||
|
GHOSTFOLIO
|
||||||
|
RAKUTEN
|
||||||
|
YAHOO
|
||||||
|
}
|
||||||
|
|
||||||
enum Provider {
|
enum Provider {
|
||||||
ANONYMOUS
|
ANONYMOUS
|
||||||
GOOGLE
|
GOOGLE
|
||||||
|
241
prisma/seed.ts
241
prisma/seed.ts
@ -1,4 +1,10 @@
|
|||||||
import { Currency, PrismaClient, Role, Type } from '@prisma/client';
|
import {
|
||||||
|
AccountType,
|
||||||
|
Currency,
|
||||||
|
PrismaClient,
|
||||||
|
Role,
|
||||||
|
Type
|
||||||
|
} from '@prisma/client';
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
@ -76,6 +82,16 @@ async function main() {
|
|||||||
create: {
|
create: {
|
||||||
accessToken:
|
accessToken:
|
||||||
'c689bcc894e4a420cb609ee34271f3e07f200594f7d199c50d75add7102889eb60061a04cd2792ebc853c54e37308271271e7bf588657c9e0c37faacbc28c3c6',
|
'c689bcc894e4a420cb609ee34271f3e07f200594f7d199c50d75add7102889eb60061a04cd2792ebc853c54e37308271271e7bf588657c9e0c37faacbc28c3c6',
|
||||||
|
Account: {
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
accountType: AccountType.SECURITIES,
|
||||||
|
id: 'f4425b66-9ba9-4ac4-93d7-fdf9a145e8cb',
|
||||||
|
isDefault: true,
|
||||||
|
name: 'Default Account'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
alias: 'Admin',
|
alias: 'Admin',
|
||||||
id: '4e1af723-95f6-44f8-92a7-464df17f6ec3',
|
id: '4e1af723-95f6-44f8-92a7-464df17f6ec3',
|
||||||
role: Role.ADMIN
|
role: Role.ADMIN
|
||||||
@ -88,106 +104,155 @@ async function main() {
|
|||||||
create: {
|
create: {
|
||||||
accessToken:
|
accessToken:
|
||||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjliMTEyYjRkLTNiN2QtNGJhZC05YmRkLTNiMGY3YjRkYWMyZiIsImlhdCI6MTYxODUxMjAxNCwiZXhwIjoxNjIxMTA0MDE0fQ.l3WUxpI0hxuQtdPrD0kd7sem6S2kx_7CrdNvkmlKuWw',
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjliMTEyYjRkLTNiN2QtNGJhZC05YmRkLTNiMGY3YjRkYWMyZiIsImlhdCI6MTYxODUxMjAxNCwiZXhwIjoxNjIxMTA0MDE0fQ.l3WUxpI0hxuQtdPrD0kd7sem6S2kx_7CrdNvkmlKuWw',
|
||||||
alias: 'Demo',
|
Account: {
|
||||||
id: '9b112b4d-3b7d-4bad-9bdd-3b0f7b4dac2f',
|
|
||||||
role: Role.DEMO,
|
|
||||||
Order: {
|
|
||||||
create: [
|
create: [
|
||||||
{
|
{
|
||||||
currency: Currency.USD,
|
accountType: AccountType.SECURITIES,
|
||||||
date: new Date(Date.UTC(2017, 0, 3, 0, 0, 0)),
|
id: 'd804de69-0429-42dc-b6ca-b308fd7dd926',
|
||||||
fee: 30,
|
name: 'Coinbase Account',
|
||||||
id: 'cf7c0418-8535-4089-ae3d-5dbfa0aec2e1',
|
platformId: platformCoinbase.id
|
||||||
platformId: platformDegiro.id,
|
|
||||||
quantity: 50,
|
|
||||||
symbol: 'TSLA',
|
|
||||||
type: Type.BUY,
|
|
||||||
unitPrice: 42.97
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
currency: Currency.USD,
|
accountType: AccountType.SECURITIES,
|
||||||
date: new Date(Date.UTC(2017, 7, 16, 0, 0, 0)),
|
id: '65cfb79d-b6c7-4591-9d46-73426bc62094',
|
||||||
fee: 29.9,
|
name: 'DEGIRO Account',
|
||||||
id: 'a1c5d73a-8631-44e5-ac44-356827a5212c',
|
platformId: platformDegiro.id
|
||||||
platformId: platformCoinbase.id,
|
|
||||||
quantity: 0.5614682,
|
|
||||||
symbol: 'BTCUSD',
|
|
||||||
type: Type.BUY,
|
|
||||||
unitPrice: 3562.089535970158
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
currency: Currency.USD,
|
accountType: AccountType.SECURITIES,
|
||||||
date: new Date(Date.UTC(2018, 9, 1, 0, 0, 0)),
|
id: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
||||||
fee: 80.79,
|
isDefault: true,
|
||||||
id: '71c08e2a-4a86-44ae-a890-c337de5d5f9b',
|
name: 'Interactive Brokers Account',
|
||||||
platformId: platformInteractiveBrokers.id,
|
platformId: platformInteractiveBrokers.id
|
||||||
quantity: 5,
|
|
||||||
symbol: 'AMZN',
|
|
||||||
type: Type.BUY,
|
|
||||||
unitPrice: 2021.99
|
|
||||||
},
|
|
||||||
{
|
|
||||||
currency: Currency.USD,
|
|
||||||
date: new Date(Date.UTC(2019, 2, 1, 0, 0, 0)),
|
|
||||||
fee: 19.9,
|
|
||||||
id: '385f2c2c-d53e-4937-b0e5-e92ef6020d4e',
|
|
||||||
platformId: platformInteractiveBrokers.id,
|
|
||||||
quantity: 10,
|
|
||||||
symbol: 'VTI',
|
|
||||||
type: Type.BUY,
|
|
||||||
unitPrice: 144.38
|
|
||||||
},
|
|
||||||
{
|
|
||||||
currency: Currency.USD,
|
|
||||||
date: new Date(Date.UTC(2019, 8, 3, 0, 0, 0)),
|
|
||||||
fee: 19.9,
|
|
||||||
id: '185f2c2c-d53e-4937-b0e5-a93ef6020d4e',
|
|
||||||
platformId: platformInteractiveBrokers.id,
|
|
||||||
quantity: 10,
|
|
||||||
symbol: 'VTI',
|
|
||||||
type: Type.BUY,
|
|
||||||
unitPrice: 147.99
|
|
||||||
},
|
|
||||||
{
|
|
||||||
currency: Currency.USD,
|
|
||||||
date: new Date(Date.UTC(2020, 2, 2, 0, 0, 0)),
|
|
||||||
fee: 19.9,
|
|
||||||
id: '347b0430-a84f-4031-a0f9-390399066ad6',
|
|
||||||
platformId: platformInteractiveBrokers.id,
|
|
||||||
quantity: 10,
|
|
||||||
symbol: 'VTI',
|
|
||||||
type: Type.BUY,
|
|
||||||
unitPrice: 151.41
|
|
||||||
},
|
|
||||||
{
|
|
||||||
currency: Currency.USD,
|
|
||||||
date: new Date(Date.UTC(2020, 8, 1, 0, 0, 0)),
|
|
||||||
fee: 19.9,
|
|
||||||
id: '67ec3f47-3189-4b63-ba05-60d3a06b302f',
|
|
||||||
platformId: platformInteractiveBrokers.id,
|
|
||||||
quantity: 10,
|
|
||||||
symbol: 'VTI',
|
|
||||||
type: Type.BUY,
|
|
||||||
unitPrice: 177.69
|
|
||||||
},
|
|
||||||
{
|
|
||||||
currency: Currency.USD,
|
|
||||||
date: new Date(Date.UTC(2020, 2, 1, 0, 0, 0)),
|
|
||||||
fee: 19.9,
|
|
||||||
id: 'd01c6fbc-fa8d-47e6-8e80-66f882d2bfd2',
|
|
||||||
platformId: platformInteractiveBrokers.id,
|
|
||||||
quantity: 10,
|
|
||||||
symbol: 'VTI',
|
|
||||||
type: Type.BUY,
|
|
||||||
unitPrice: 203.15
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
alias: 'Demo',
|
||||||
|
id: '9b112b4d-3b7d-4bad-9bdd-3b0f7b4dac2f',
|
||||||
|
role: Role.DEMO
|
||||||
},
|
},
|
||||||
update: {},
|
update: {},
|
||||||
where: { id: '9b112b4d-3b7d-4bad-9bdd-3b0f7b4dac2f' }
|
where: { id: '9b112b4d-3b7d-4bad-9bdd-3b0f7b4dac2f' }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await prisma.order.createMany({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
accountId: '65cfb79d-b6c7-4591-9d46-73426bc62094',
|
||||||
|
accountUserId: userDemo.id,
|
||||||
|
currency: Currency.USD,
|
||||||
|
date: new Date(Date.UTC(2017, 0, 3, 0, 0, 0)),
|
||||||
|
fee: 30,
|
||||||
|
id: 'cf7c0418-8535-4089-ae3d-5dbfa0aec2e1',
|
||||||
|
platformId: platformDegiro.id,
|
||||||
|
quantity: 50,
|
||||||
|
symbol: 'TSLA',
|
||||||
|
type: Type.BUY,
|
||||||
|
unitPrice: 42.97,
|
||||||
|
userId: userDemo.id
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accountId: 'd804de69-0429-42dc-b6ca-b308fd7dd926',
|
||||||
|
accountUserId: userDemo.id,
|
||||||
|
currency: Currency.USD,
|
||||||
|
date: new Date(Date.UTC(2017, 7, 16, 0, 0, 0)),
|
||||||
|
fee: 29.9,
|
||||||
|
id: 'a1c5d73a-8631-44e5-ac44-356827a5212c',
|
||||||
|
platformId: platformCoinbase.id,
|
||||||
|
quantity: 0.5614682,
|
||||||
|
symbol: 'BTCUSD',
|
||||||
|
type: Type.BUY,
|
||||||
|
unitPrice: 3562.089535970158,
|
||||||
|
userId: userDemo.id
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
||||||
|
accountUserId: userDemo.id,
|
||||||
|
currency: Currency.USD,
|
||||||
|
date: new Date(Date.UTC(2018, 9, 1, 0, 0, 0)),
|
||||||
|
fee: 80.79,
|
||||||
|
id: '71c08e2a-4a86-44ae-a890-c337de5d5f9b',
|
||||||
|
platformId: platformInteractiveBrokers.id,
|
||||||
|
quantity: 5,
|
||||||
|
symbol: 'AMZN',
|
||||||
|
type: Type.BUY,
|
||||||
|
unitPrice: 2021.99,
|
||||||
|
userId: userDemo.id
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
||||||
|
accountUserId: userDemo.id,
|
||||||
|
currency: Currency.USD,
|
||||||
|
date: new Date(Date.UTC(2019, 2, 1, 0, 0, 0)),
|
||||||
|
fee: 19.9,
|
||||||
|
id: '385f2c2c-d53e-4937-b0e5-e92ef6020d4e',
|
||||||
|
platformId: platformInteractiveBrokers.id,
|
||||||
|
quantity: 10,
|
||||||
|
symbol: 'VTI',
|
||||||
|
type: Type.BUY,
|
||||||
|
unitPrice: 144.38,
|
||||||
|
userId: userDemo.id
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
||||||
|
accountUserId: userDemo.id,
|
||||||
|
currency: Currency.USD,
|
||||||
|
date: new Date(Date.UTC(2019, 8, 3, 0, 0, 0)),
|
||||||
|
fee: 19.9,
|
||||||
|
id: '185f2c2c-d53e-4937-b0e5-a93ef6020d4e',
|
||||||
|
platformId: platformInteractiveBrokers.id,
|
||||||
|
quantity: 10,
|
||||||
|
symbol: 'VTI',
|
||||||
|
type: Type.BUY,
|
||||||
|
unitPrice: 147.99,
|
||||||
|
userId: userDemo.id
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
||||||
|
accountUserId: userDemo.id,
|
||||||
|
currency: Currency.USD,
|
||||||
|
date: new Date(Date.UTC(2020, 2, 2, 0, 0, 0)),
|
||||||
|
fee: 19.9,
|
||||||
|
id: '347b0430-a84f-4031-a0f9-390399066ad6',
|
||||||
|
platformId: platformInteractiveBrokers.id,
|
||||||
|
quantity: 10,
|
||||||
|
symbol: 'VTI',
|
||||||
|
type: Type.BUY,
|
||||||
|
unitPrice: 151.41,
|
||||||
|
userId: userDemo.id
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
||||||
|
accountUserId: userDemo.id,
|
||||||
|
currency: Currency.USD,
|
||||||
|
date: new Date(Date.UTC(2020, 8, 1, 0, 0, 0)),
|
||||||
|
fee: 19.9,
|
||||||
|
id: '67ec3f47-3189-4b63-ba05-60d3a06b302f',
|
||||||
|
platformId: platformInteractiveBrokers.id,
|
||||||
|
quantity: 10,
|
||||||
|
symbol: 'VTI',
|
||||||
|
type: Type.BUY,
|
||||||
|
unitPrice: 177.69,
|
||||||
|
userId: userDemo.id
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accountId: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
|
||||||
|
accountUserId: userDemo.id,
|
||||||
|
currency: Currency.USD,
|
||||||
|
date: new Date(Date.UTC(2020, 2, 1, 0, 0, 0)),
|
||||||
|
fee: 19.9,
|
||||||
|
id: 'd01c6fbc-fa8d-47e6-8e80-66f882d2bfd2',
|
||||||
|
platformId: platformInteractiveBrokers.id,
|
||||||
|
quantity: 10,
|
||||||
|
symbol: 'VTI',
|
||||||
|
type: Type.BUY,
|
||||||
|
unitPrice: 203.15,
|
||||||
|
userId: userDemo.id
|
||||||
|
}
|
||||||
|
],
|
||||||
|
skipDuplicates: true
|
||||||
|
});
|
||||||
|
|
||||||
console.log({
|
console.log({
|
||||||
platformBitcoinSuisse,
|
platformBitcoinSuisse,
|
||||||
platformBitpanda,
|
platformBitpanda,
|
||||||
|
Reference in New Issue
Block a user