Feature/create and update accounts (#60)
* Allow to create and update accounts * Activate account selector in transaction dialog * Refactor analytics and report from platforms to accounts
This commit is contained in:
parent
d17b02092e
commit
90a2fea7d6
@ -5,6 +5,12 @@ 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).
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the logic to create and update accounts
|
||||||
|
|
||||||
## 0.97.0 - 01.05.2021
|
## 0.97.0 - 01.05.2021
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -17,7 +17,7 @@ import {
|
|||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { Account as AccountModel } from '@prisma/client';
|
import { Account as AccountModel, Order } from '@prisma/client';
|
||||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
|
||||||
import { AccountService } from './account.service';
|
import { AccountService } from './account.service';
|
||||||
@ -47,6 +47,23 @@ export class AccountController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const account = await this.accountService.accountWithOrders(
|
||||||
|
{
|
||||||
|
id_userId: {
|
||||||
|
id,
|
||||||
|
userId: this.request.user.id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ Order: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (account?.isDefault || account?.Order.length > 0) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
|
StatusCodes.FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return this.accountService.deleteAccount(
|
return this.accountService.deleteAccount(
|
||||||
{
|
{
|
||||||
id_userId: {
|
id_userId: {
|
||||||
@ -69,8 +86,8 @@ export class AccountController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let accounts = await this.accountService.accounts({
|
let accounts = await this.accountService.accounts({
|
||||||
include: { Platform: true },
|
include: { Order: true, Platform: true },
|
||||||
orderBy: { name: 'desc' },
|
orderBy: { name: 'asc' },
|
||||||
where: { userId: impersonationUserId || this.request.user.id }
|
where: { userId: impersonationUserId || this.request.user.id }
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -166,6 +183,13 @@ export class AccountController {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!originalAccount) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
|
StatusCodes.FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (data.platformId) {
|
if (data.platformId) {
|
||||||
const platformId = data.platformId;
|
const platformId = data.platformId;
|
||||||
delete data.platformId;
|
delete data.platformId;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Account, Prisma } from '@prisma/client';
|
import { Account, Order, Prisma } from '@prisma/client';
|
||||||
|
|
||||||
import { RedisCacheService } from '../redis-cache/redis-cache.service';
|
import { RedisCacheService } from '../redis-cache/redis-cache.service';
|
||||||
|
|
||||||
@ -19,6 +19,20 @@ export class AccountService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async accountWithOrders(
|
||||||
|
accountWhereUniqueInput: Prisma.AccountWhereUniqueInput,
|
||||||
|
accountInclude: Prisma.AccountInclude
|
||||||
|
): Promise<
|
||||||
|
Account & {
|
||||||
|
Order?: Order[];
|
||||||
|
}
|
||||||
|
> {
|
||||||
|
return this.prisma.account.findUnique({
|
||||||
|
include: accountInclude,
|
||||||
|
where: accountWhereUniqueInput
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public async accounts(params: {
|
public async accounts(params: {
|
||||||
include?: Prisma.AccountInclude;
|
include?: Prisma.AccountInclude;
|
||||||
skip?: number;
|
skip?: number;
|
||||||
|
@ -1,35 +1,14 @@
|
|||||||
import { Currency, DataSource, Type } from '@prisma/client';
|
import { AccountType } from '@prisma/client';
|
||||||
import { IsISO8601, IsNumber, IsString, ValidateIf } from 'class-validator';
|
import { IsString, ValidateIf } from 'class-validator';
|
||||||
|
|
||||||
export class CreateAccountDto {
|
export class CreateAccountDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
accountId: string;
|
accountType: AccountType;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
currency: Currency;
|
name: string;
|
||||||
|
|
||||||
@IsString()
|
|
||||||
dataSource: DataSource;
|
|
||||||
|
|
||||||
@IsISO8601()
|
|
||||||
date: string;
|
|
||||||
|
|
||||||
@IsNumber()
|
|
||||||
fee: number;
|
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@ValidateIf((object, value) => value !== null)
|
@ValidateIf((object, value) => value !== null)
|
||||||
platformId: string | null;
|
platformId: string | null;
|
||||||
|
|
||||||
@IsNumber()
|
|
||||||
quantity: number;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
symbol: string;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
type: Type;
|
|
||||||
|
|
||||||
@IsNumber()
|
|
||||||
unitPrice: number;
|
|
||||||
}
|
}
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
import { Order, Platform } from '@prisma/client';
|
|
||||||
|
|
||||||
export type OrderWithPlatform = Order & { Platform?: Platform };
|
|
@ -1,38 +1,17 @@
|
|||||||
import { Currency, DataSource, Type } from '@prisma/client';
|
import { AccountType } from '@prisma/client';
|
||||||
import { IsISO8601, IsNumber, IsString, ValidateIf } from 'class-validator';
|
import { IsString, ValidateIf } from 'class-validator';
|
||||||
|
|
||||||
export class UpdateAccountDto {
|
export class UpdateAccountDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
accountId: string;
|
accountType: AccountType;
|
||||||
|
|
||||||
@IsString()
|
|
||||||
currency: Currency;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
dataSource: DataSource;
|
|
||||||
|
|
||||||
@IsISO8601()
|
|
||||||
date: string;
|
|
||||||
|
|
||||||
@IsNumber()
|
|
||||||
fee: number;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@ValidateIf((object, value) => value !== null)
|
|
||||||
platformId: string | null;
|
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
@IsNumber()
|
@IsString()
|
||||||
quantity: number;
|
name: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
symbol: string;
|
@ValidateIf((object, value) => value !== null)
|
||||||
|
platformId: string | null;
|
||||||
@IsString()
|
|
||||||
type: Type;
|
|
||||||
|
|
||||||
@IsNumber()
|
|
||||||
unitPrice: number;
|
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { Currency, Type } from '@prisma/client';
|
import { Currency, Type } from '@prisma/client';
|
||||||
import { parseISO } from 'date-fns';
|
import { parseISO } from 'date-fns';
|
||||||
|
|
||||||
import { OrderWithPlatform } from '../order/interfaces/order-with-platform.type';
|
import { OrderWithAccount } from '../order/interfaces/order-with-account.type';
|
||||||
import { CreateOrderDto } from './create-order.dto';
|
import { CreateOrderDto } from './create-order.dto';
|
||||||
import { Data } from './interfaces/data.interface';
|
import { Data } from './interfaces/data.interface';
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ export class ExperimentalService {
|
|||||||
aDate: Date,
|
aDate: Date,
|
||||||
aBaseCurrency: Currency
|
aBaseCurrency: Currency
|
||||||
): Promise<Data> {
|
): Promise<Data> {
|
||||||
const ordersWithPlatform: OrderWithPlatform[] = aOrders.map((order) => {
|
const ordersWithPlatform: OrderWithAccount[] = aOrders.map((order) => {
|
||||||
return {
|
return {
|
||||||
...order,
|
...order,
|
||||||
accountId: undefined,
|
accountId: undefined,
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
import { Account, Order } from '@prisma/client';
|
||||||
|
|
||||||
|
export type OrderWithAccount = Order & { Account?: Account };
|
@ -1,3 +0,0 @@
|
|||||||
import { Order, Platform } from '@prisma/client';
|
|
||||||
|
|
||||||
export type OrderWithPlatform = Order & { Platform?: Platform };
|
|
@ -184,6 +184,13 @@ export class OrderController {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!originalOrder) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
|
StatusCodes.FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const date = parseISO(data.date);
|
const date = parseISO(data.date);
|
||||||
|
|
||||||
const accountId = data.accountId;
|
const accountId = data.accountId;
|
||||||
|
@ -5,7 +5,7 @@ import { Order, Prisma } from '@prisma/client';
|
|||||||
|
|
||||||
import { CacheService } from '../cache/cache.service';
|
import { CacheService } from '../cache/cache.service';
|
||||||
import { RedisCacheService } from '../redis-cache/redis-cache.service';
|
import { RedisCacheService } from '../redis-cache/redis-cache.service';
|
||||||
import { OrderWithPlatform } from './interfaces/order-with-platform.type';
|
import { OrderWithAccount } from './interfaces/order-with-account.type';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OrderService {
|
export class OrderService {
|
||||||
@ -31,7 +31,7 @@ export class OrderService {
|
|||||||
cursor?: Prisma.OrderWhereUniqueInput;
|
cursor?: Prisma.OrderWhereUniqueInput;
|
||||||
where?: Prisma.OrderWhereInput;
|
where?: Prisma.OrderWhereInput;
|
||||||
orderBy?: Prisma.OrderOrderByInput;
|
orderBy?: Prisma.OrderOrderByInput;
|
||||||
}): Promise<OrderWithPlatform[]> {
|
}): Promise<OrderWithAccount[]> {
|
||||||
const { include, skip, take, cursor, where, orderBy } = params;
|
const { include, skip, take, cursor, where, orderBy } = params;
|
||||||
|
|
||||||
return this.prisma.order.findMany({
|
return this.prisma.order.findMany({
|
||||||
|
@ -2,6 +2,9 @@ import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces';
|
|||||||
import { Currency } from '@prisma/client';
|
import { Currency } from '@prisma/client';
|
||||||
|
|
||||||
export interface PortfolioPosition {
|
export interface PortfolioPosition {
|
||||||
|
accounts: {
|
||||||
|
[name: string]: { current: number; original: number };
|
||||||
|
};
|
||||||
currency: Currency;
|
currency: Currency;
|
||||||
exchange?: string;
|
exchange?: string;
|
||||||
grossPerformance: number;
|
grossPerformance: number;
|
||||||
@ -13,9 +16,6 @@ export interface PortfolioPosition {
|
|||||||
marketPrice: number;
|
marketPrice: number;
|
||||||
marketState: MarketState;
|
marketState: MarketState;
|
||||||
name: string;
|
name: string;
|
||||||
platforms: {
|
|
||||||
[name: string]: { current: number; original: number };
|
|
||||||
};
|
|
||||||
quantity: number;
|
quantity: number;
|
||||||
sector?: string;
|
sector?: string;
|
||||||
shareCurrent: number;
|
shareCurrent: number;
|
||||||
|
@ -185,11 +185,11 @@ export class PortfolioController {
|
|||||||
portfolioPosition.investment =
|
portfolioPosition.investment =
|
||||||
portfolioPosition.investment / totalInvestment;
|
portfolioPosition.investment / totalInvestment;
|
||||||
|
|
||||||
for (const [platform, { current, original }] of Object.entries(
|
for (const [account, { current, original }] of Object.entries(
|
||||||
portfolioPosition.platforms
|
portfolioPosition.accounts
|
||||||
)) {
|
)) {
|
||||||
portfolioPosition.platforms[platform].current = current / totalValue;
|
portfolioPosition.accounts[account].current = current / totalValue;
|
||||||
portfolioPosition.platforms[platform].original =
|
portfolioPosition.accounts[account].original =
|
||||||
original / totalInvestment;
|
original / totalInvestment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ export class PortfolioService {
|
|||||||
// Get portfolio from database
|
// Get portfolio from database
|
||||||
const orders = await this.orderService.orders({
|
const orders = await this.orderService.orders({
|
||||||
include: {
|
include: {
|
||||||
Platform: true
|
Account: true
|
||||||
},
|
},
|
||||||
orderBy: { date: 'asc' },
|
orderBy: { date: 'asc' },
|
||||||
where: { userId: aUserId }
|
where: { userId: aUserId }
|
||||||
|
@ -1,27 +1,27 @@
|
|||||||
import { Currency, Platform } from '@prisma/client';
|
import { Account, Currency, Platform } from '@prisma/client';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import { IOrder } from '../services/interfaces/interfaces';
|
import { IOrder } from '../services/interfaces/interfaces';
|
||||||
import { OrderType } from './order-type';
|
import { OrderType } from './order-type';
|
||||||
|
|
||||||
export class Order {
|
export class Order {
|
||||||
|
private account: Account;
|
||||||
private currency: Currency;
|
private currency: Currency;
|
||||||
private fee: number;
|
private fee: number;
|
||||||
private date: string;
|
private date: string;
|
||||||
private id: string;
|
private id: string;
|
||||||
private quantity: number;
|
private quantity: number;
|
||||||
private platform: Platform;
|
|
||||||
private symbol: string;
|
private symbol: string;
|
||||||
private total: number;
|
private total: number;
|
||||||
private type: OrderType;
|
private type: OrderType;
|
||||||
private unitPrice: number;
|
private unitPrice: number;
|
||||||
|
|
||||||
public constructor(data: IOrder) {
|
public constructor(data: IOrder) {
|
||||||
|
this.account = data.account;
|
||||||
this.currency = data.currency;
|
this.currency = data.currency;
|
||||||
this.fee = data.fee;
|
this.fee = data.fee;
|
||||||
this.date = data.date;
|
this.date = data.date;
|
||||||
this.id = data.id || uuidv4();
|
this.id = data.id || uuidv4();
|
||||||
this.platform = data.platform;
|
|
||||||
this.quantity = data.quantity;
|
this.quantity = data.quantity;
|
||||||
this.symbol = data.symbol;
|
this.symbol = data.symbol;
|
||||||
this.type = data.type;
|
this.type = data.type;
|
||||||
@ -30,6 +30,10 @@ export class Order {
|
|||||||
this.total = this.quantity * data.unitPrice;
|
this.total = this.quantity * data.unitPrice;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getAccount() {
|
||||||
|
return this.account;
|
||||||
|
}
|
||||||
|
|
||||||
public getCurrency() {
|
public getCurrency() {
|
||||||
return this.currency;
|
return this.currency;
|
||||||
}
|
}
|
||||||
@ -46,10 +50,6 @@ export class Order {
|
|||||||
return this.id;
|
return this.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPlatform() {
|
|
||||||
return this.platform;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getQuantity() {
|
public getQuantity() {
|
||||||
return this.quantity;
|
return this.quantity;
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ import { cloneDeep, isEmpty } from 'lodash';
|
|||||||
import * as roundTo from 'round-to';
|
import * as roundTo from 'round-to';
|
||||||
|
|
||||||
import { UserWithSettings } from '../app/interfaces/user-with-settings';
|
import { UserWithSettings } from '../app/interfaces/user-with-settings';
|
||||||
import { OrderWithPlatform } from '../app/order/interfaces/order-with-platform.type';
|
import { OrderWithAccount } from '../app/order/interfaces/order-with-account.type';
|
||||||
import { DateRange } from '../app/portfolio/interfaces/date-range.type';
|
import { DateRange } from '../app/portfolio/interfaces/date-range.type';
|
||||||
import { PortfolioPerformance } from '../app/portfolio/interfaces/portfolio-performance.interface';
|
import { PortfolioPerformance } from '../app/portfolio/interfaces/portfolio-performance.interface';
|
||||||
import { PortfolioPosition } from '../app/portfolio/interfaces/portfolio-position.interface';
|
import { PortfolioPosition } from '../app/portfolio/interfaces/portfolio-position.interface';
|
||||||
@ -34,14 +34,14 @@ import { IOrder } from '../services/interfaces/interfaces';
|
|||||||
import { RulesService } from '../services/rules.service';
|
import { RulesService } from '../services/rules.service';
|
||||||
import { PortfolioInterface } from './interfaces/portfolio.interface';
|
import { PortfolioInterface } from './interfaces/portfolio.interface';
|
||||||
import { Order } from './order';
|
import { Order } from './order';
|
||||||
|
import { AccountClusterRiskCurrentInvestment } from './rules/account-cluster-risk/current-investment';
|
||||||
|
import { AccountClusterRiskInitialInvestment } from './rules/account-cluster-risk/initial-investment';
|
||||||
|
import { AccountClusterRiskSingleAccount } from './rules/account-cluster-risk/single-account';
|
||||||
import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from './rules/currency-cluster-risk/base-currency-current-investment';
|
import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from './rules/currency-cluster-risk/base-currency-current-investment';
|
||||||
import { CurrencyClusterRiskBaseCurrencyInitialInvestment } from './rules/currency-cluster-risk/base-currency-initial-investment';
|
import { CurrencyClusterRiskBaseCurrencyInitialInvestment } from './rules/currency-cluster-risk/base-currency-initial-investment';
|
||||||
import { CurrencyClusterRiskCurrentInvestment } from './rules/currency-cluster-risk/current-investment';
|
import { CurrencyClusterRiskCurrentInvestment } from './rules/currency-cluster-risk/current-investment';
|
||||||
import { CurrencyClusterRiskInitialInvestment } from './rules/currency-cluster-risk/initial-investment';
|
import { CurrencyClusterRiskInitialInvestment } from './rules/currency-cluster-risk/initial-investment';
|
||||||
import { FeeRatioInitialInvestment } from './rules/fees/fee-ratio-initial-investment';
|
import { FeeRatioInitialInvestment } from './rules/fees/fee-ratio-initial-investment';
|
||||||
import { PlatformClusterRiskCurrentInvestment } from './rules/platform-cluster-risk/current-investment';
|
|
||||||
import { PlatformClusterRiskInitialInvestment } from './rules/platform-cluster-risk/initial-investment';
|
|
||||||
import { PlatformClusterRiskSinglePlatform } from './rules/platform-cluster-risk/single-platform';
|
|
||||||
|
|
||||||
export class Portfolio implements PortfolioInterface {
|
export class Portfolio implements PortfolioInterface {
|
||||||
private orders: Order[] = [];
|
private orders: Order[] = [];
|
||||||
@ -119,11 +119,11 @@ export class Portfolio implements PortfolioInterface {
|
|||||||
}): Portfolio {
|
}): Portfolio {
|
||||||
orders.forEach(
|
orders.forEach(
|
||||||
({
|
({
|
||||||
|
account,
|
||||||
currency,
|
currency,
|
||||||
fee,
|
fee,
|
||||||
date,
|
date,
|
||||||
id,
|
id,
|
||||||
platform,
|
|
||||||
quantity,
|
quantity,
|
||||||
symbol,
|
symbol,
|
||||||
type,
|
type,
|
||||||
@ -131,11 +131,11 @@ export class Portfolio implements PortfolioInterface {
|
|||||||
}) => {
|
}) => {
|
||||||
this.orders.push(
|
this.orders.push(
|
||||||
new Order({
|
new Order({
|
||||||
|
account,
|
||||||
currency,
|
currency,
|
||||||
fee,
|
fee,
|
||||||
date,
|
date,
|
||||||
id,
|
id,
|
||||||
platform,
|
|
||||||
quantity,
|
quantity,
|
||||||
symbol,
|
symbol,
|
||||||
type,
|
type,
|
||||||
@ -202,7 +202,7 @@ export class Portfolio implements PortfolioInterface {
|
|||||||
const data = await this.dataProviderService.get(symbols);
|
const data = await this.dataProviderService.get(symbols);
|
||||||
|
|
||||||
symbols.forEach((symbol) => {
|
symbols.forEach((symbol) => {
|
||||||
const platforms: PortfolioPosition['platforms'] = {};
|
const accounts: PortfolioPosition['accounts'] = {};
|
||||||
const [portfolioItem] = portfolioItems;
|
const [portfolioItem] = portfolioItems;
|
||||||
|
|
||||||
const ordersBySymbol = this.getOrders().filter((order) => {
|
const ordersBySymbol = this.getOrders().filter((order) => {
|
||||||
@ -227,15 +227,15 @@ export class Portfolio implements PortfolioInterface {
|
|||||||
originalValueOfSymbol *= -1;
|
originalValueOfSymbol *= -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (platforms[orderOfSymbol.getPlatform()?.name || 'Other']?.current) {
|
if (accounts[orderOfSymbol.getAccount()?.name || 'Other']?.current) {
|
||||||
platforms[
|
accounts[
|
||||||
orderOfSymbol.getPlatform()?.name || 'Other'
|
orderOfSymbol.getAccount()?.name || 'Other'
|
||||||
].current += currentValueOfSymbol;
|
].current += currentValueOfSymbol;
|
||||||
platforms[
|
accounts[
|
||||||
orderOfSymbol.getPlatform()?.name || 'Other'
|
orderOfSymbol.getAccount()?.name || 'Other'
|
||||||
].original += originalValueOfSymbol;
|
].original += originalValueOfSymbol;
|
||||||
} else {
|
} else {
|
||||||
platforms[orderOfSymbol.getPlatform()?.name || 'Other'] = {
|
accounts[orderOfSymbol.getAccount()?.name || 'Other'] = {
|
||||||
current: currentValueOfSymbol,
|
current: currentValueOfSymbol,
|
||||||
original: originalValueOfSymbol
|
original: originalValueOfSymbol
|
||||||
};
|
};
|
||||||
@ -276,7 +276,7 @@ export class Portfolio implements PortfolioInterface {
|
|||||||
|
|
||||||
details[symbol] = {
|
details[symbol] = {
|
||||||
...data[symbol],
|
...data[symbol],
|
||||||
platforms,
|
accounts,
|
||||||
symbol,
|
symbol,
|
||||||
grossPerformance: roundTo(
|
grossPerformance: roundTo(
|
||||||
portfolioItemsNow.positions[symbol].quantity * (now - before),
|
portfolioItemsNow.positions[symbol].quantity * (now - before),
|
||||||
@ -396,6 +396,19 @@ export class Portfolio implements PortfolioInterface {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
rules: {
|
rules: {
|
||||||
|
accountClusterRisk: await this.rulesService.evaluate(
|
||||||
|
this,
|
||||||
|
[
|
||||||
|
new AccountClusterRiskCurrentInvestment(
|
||||||
|
this.exchangeRateDataService
|
||||||
|
),
|
||||||
|
new AccountClusterRiskInitialInvestment(
|
||||||
|
this.exchangeRateDataService
|
||||||
|
),
|
||||||
|
new AccountClusterRiskSingleAccount(this.exchangeRateDataService)
|
||||||
|
],
|
||||||
|
{ baseCurrency: this.user.Settings.currency }
|
||||||
|
),
|
||||||
currencyClusterRisk: await this.rulesService.evaluate(
|
currencyClusterRisk: await this.rulesService.evaluate(
|
||||||
this,
|
this,
|
||||||
[
|
[
|
||||||
@ -414,19 +427,6 @@ export class Portfolio implements PortfolioInterface {
|
|||||||
],
|
],
|
||||||
{ baseCurrency: this.user.Settings.currency }
|
{ baseCurrency: this.user.Settings.currency }
|
||||||
),
|
),
|
||||||
platformClusterRisk: await this.rulesService.evaluate(
|
|
||||||
this,
|
|
||||||
[
|
|
||||||
new PlatformClusterRiskSinglePlatform(this.exchangeRateDataService),
|
|
||||||
new PlatformClusterRiskInitialInvestment(
|
|
||||||
this.exchangeRateDataService
|
|
||||||
),
|
|
||||||
new PlatformClusterRiskCurrentInvestment(
|
|
||||||
this.exchangeRateDataService
|
|
||||||
)
|
|
||||||
],
|
|
||||||
{ baseCurrency: this.user.Settings.currency }
|
|
||||||
),
|
|
||||||
fees: await this.rulesService.evaluate(
|
fees: await this.rulesService.evaluate(
|
||||||
this,
|
this,
|
||||||
[new FeeRatioInitialInvestment(this.exchangeRateDataService)],
|
[new FeeRatioInitialInvestment(this.exchangeRateDataService)],
|
||||||
@ -522,17 +522,17 @@ export class Portfolio implements PortfolioInterface {
|
|||||||
return isFinite(value) ? value : null;
|
return isFinite(value) ? value : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setOrders(aOrders: OrderWithPlatform[]) {
|
public async setOrders(aOrders: OrderWithAccount[]) {
|
||||||
this.orders = [];
|
this.orders = [];
|
||||||
|
|
||||||
// Map data
|
// Map data
|
||||||
aOrders.forEach((order) => {
|
aOrders.forEach((order) => {
|
||||||
this.orders.push(
|
this.orders.push(
|
||||||
new Order({
|
new Order({
|
||||||
|
account: order.Account,
|
||||||
currency: <any>order.currency,
|
currency: <any>order.currency,
|
||||||
date: order.date.toISOString(),
|
date: order.date.toISOString(),
|
||||||
fee: order.fee,
|
fee: order.fee,
|
||||||
platform: order.Platform,
|
|
||||||
quantity: order.quantity,
|
quantity: order.quantity,
|
||||||
symbol: order.symbol,
|
symbol: order.symbol,
|
||||||
type: <any>order.type,
|
type: <any>order.type,
|
||||||
|
@ -3,7 +3,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
|
|||||||
|
|
||||||
import { Rule } from '../../rule';
|
import { Rule } from '../../rule';
|
||||||
|
|
||||||
export class PlatformClusterRiskCurrentInvestment extends Rule {
|
export class AccountClusterRiskCurrentInvestment extends Rule {
|
||||||
public constructor(public exchangeRateDataService: ExchangeRateDataService) {
|
public constructor(public exchangeRateDataService: ExchangeRateDataService) {
|
||||||
super(exchangeRateDataService, {
|
super(exchangeRateDataService, {
|
||||||
name: 'Current Investment'
|
name: 'Current Investment'
|
||||||
@ -18,24 +18,22 @@ export class PlatformClusterRiskCurrentInvestment extends Rule {
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const ruleSettings =
|
const ruleSettings =
|
||||||
aRuleSettingsMap[PlatformClusterRiskCurrentInvestment.name];
|
aRuleSettingsMap[AccountClusterRiskCurrentInvestment.name];
|
||||||
|
|
||||||
const platforms: {
|
const accounts: {
|
||||||
[symbol: string]: Pick<PortfolioPosition, 'name'> & {
|
[symbol: string]: Pick<PortfolioPosition, 'name'> & {
|
||||||
investment: number;
|
investment: number;
|
||||||
};
|
};
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
Object.values(aPositions).forEach((position) => {
|
Object.values(aPositions).forEach((position) => {
|
||||||
for (const [platform, { current }] of Object.entries(
|
for (const [account, { current }] of Object.entries(position.accounts)) {
|
||||||
position.platforms
|
if (accounts[account]?.investment) {
|
||||||
)) {
|
accounts[account].investment += current;
|
||||||
if (platforms[platform]?.investment) {
|
|
||||||
platforms[platform].investment += current;
|
|
||||||
} else {
|
} else {
|
||||||
platforms[platform] = {
|
accounts[account] = {
|
||||||
investment: current,
|
investment: current,
|
||||||
name: platform
|
name: account
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -44,17 +42,17 @@ export class PlatformClusterRiskCurrentInvestment extends Rule {
|
|||||||
let maxItem;
|
let maxItem;
|
||||||
let totalInvestment = 0;
|
let totalInvestment = 0;
|
||||||
|
|
||||||
Object.values(platforms).forEach((platform) => {
|
Object.values(accounts).forEach((account) => {
|
||||||
if (!maxItem) {
|
if (!maxItem) {
|
||||||
maxItem = platform;
|
maxItem = account;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate total investment
|
// Calculate total investment
|
||||||
totalInvestment += platform.investment;
|
totalInvestment += account.investment;
|
||||||
|
|
||||||
// Find maximum
|
// Find maximum
|
||||||
if (platform.investment > maxItem?.investment) {
|
if (account.investment > maxItem?.investment) {
|
||||||
maxItem = platform;
|
maxItem = account;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -3,7 +3,7 @@ import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-dat
|
|||||||
|
|
||||||
import { Rule } from '../../rule';
|
import { Rule } from '../../rule';
|
||||||
|
|
||||||
export class PlatformClusterRiskInitialInvestment extends Rule {
|
export class AccountClusterRiskInitialInvestment extends Rule {
|
||||||
public constructor(public exchangeRateDataService: ExchangeRateDataService) {
|
public constructor(public exchangeRateDataService: ExchangeRateDataService) {
|
||||||
super(exchangeRateDataService, {
|
super(exchangeRateDataService, {
|
||||||
name: 'Initial Investment'
|
name: 'Initial Investment'
|
||||||
@ -18,7 +18,7 @@ export class PlatformClusterRiskInitialInvestment extends Rule {
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const ruleSettings =
|
const ruleSettings =
|
||||||
aRuleSettingsMap[PlatformClusterRiskInitialInvestment.name];
|
aRuleSettingsMap[AccountClusterRiskInitialInvestment.name];
|
||||||
|
|
||||||
const platforms: {
|
const platforms: {
|
||||||
[symbol: string]: Pick<PortfolioPosition, 'name'> & {
|
[symbol: string]: Pick<PortfolioPosition, 'name'> & {
|
||||||
@ -27,15 +27,13 @@ export class PlatformClusterRiskInitialInvestment extends Rule {
|
|||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
Object.values(aPositions).forEach((position) => {
|
Object.values(aPositions).forEach((position) => {
|
||||||
for (const [platform, { original }] of Object.entries(
|
for (const [account, { original }] of Object.entries(position.accounts)) {
|
||||||
position.platforms
|
if (platforms[account]?.investment) {
|
||||||
)) {
|
platforms[account].investment += original;
|
||||||
if (platforms[platform]?.investment) {
|
|
||||||
platforms[platform].investment += original;
|
|
||||||
} else {
|
} else {
|
||||||
platforms[platform] = {
|
platforms[account] = {
|
||||||
investment: original,
|
investment: original,
|
||||||
name: platform
|
name: account
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,33 +3,33 @@ import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-dat
|
|||||||
|
|
||||||
import { Rule } from '../../rule';
|
import { Rule } from '../../rule';
|
||||||
|
|
||||||
export class PlatformClusterRiskSinglePlatform extends Rule {
|
export class AccountClusterRiskSingleAccount extends Rule {
|
||||||
public constructor(public exchangeRateDataService: ExchangeRateDataService) {
|
public constructor(public exchangeRateDataService: ExchangeRateDataService) {
|
||||||
super(exchangeRateDataService, {
|
super(exchangeRateDataService, {
|
||||||
name: 'Single Platform'
|
name: 'Single Account'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public evaluate(positions: { [symbol: string]: PortfolioPosition }) {
|
public evaluate(positions: { [symbol: string]: PortfolioPosition }) {
|
||||||
const platforms: string[] = [];
|
const accounts: string[] = [];
|
||||||
|
|
||||||
Object.values(positions).forEach((position) => {
|
Object.values(positions).forEach((position) => {
|
||||||
for (const [platform] of Object.entries(position.platforms)) {
|
for (const [account] of Object.entries(position.accounts)) {
|
||||||
if (!platforms.includes(platform)) {
|
if (!accounts.includes(account)) {
|
||||||
platforms.push(platform);
|
accounts.push(account);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (platforms.length === 1) {
|
if (accounts.length === 1) {
|
||||||
return {
|
return {
|
||||||
evaluation: `All your investment is managed by a single platform`,
|
evaluation: `All your investment is managed by a single account`,
|
||||||
value: false
|
value: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
evaluation: `Your investment is managed by ${platforms.length} platforms`,
|
evaluation: `Your investment is managed by ${accounts.length} accounts`,
|
||||||
value: true
|
value: true
|
||||||
};
|
};
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { Currency, DataSource, Platform } from '@prisma/client';
|
import { Account, Currency, DataSource, Platform } from '@prisma/client';
|
||||||
|
|
||||||
import { OrderType } from '../../models/order-type';
|
import { OrderType } from '../../models/order-type';
|
||||||
|
|
||||||
@ -33,11 +33,11 @@ export const Type = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface IOrder {
|
export interface IOrder {
|
||||||
|
account: Account;
|
||||||
currency: Currency;
|
currency: Currency;
|
||||||
date: string;
|
date: string;
|
||||||
fee: number;
|
fee: number;
|
||||||
id?: string;
|
id?: string;
|
||||||
platform: Platform;
|
|
||||||
quantity: number;
|
quantity: number;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
type: OrderType;
|
type: OrderType;
|
||||||
|
@ -2,14 +2,14 @@ import { Injectable } from '@nestjs/common';
|
|||||||
|
|
||||||
import { Portfolio } from '../models/portfolio';
|
import { Portfolio } from '../models/portfolio';
|
||||||
import { Rule } from '../models/rule';
|
import { Rule } from '../models/rule';
|
||||||
|
import { AccountClusterRiskCurrentInvestment } from '../models/rules/account-cluster-risk/current-investment';
|
||||||
|
import { AccountClusterRiskInitialInvestment } from '../models/rules/account-cluster-risk/initial-investment';
|
||||||
|
import { AccountClusterRiskSingleAccount } from '../models/rules/account-cluster-risk/single-account';
|
||||||
import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from '../models/rules/currency-cluster-risk/base-currency-current-investment';
|
import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from '../models/rules/currency-cluster-risk/base-currency-current-investment';
|
||||||
import { CurrencyClusterRiskBaseCurrencyInitialInvestment } from '../models/rules/currency-cluster-risk/base-currency-initial-investment';
|
import { CurrencyClusterRiskBaseCurrencyInitialInvestment } from '../models/rules/currency-cluster-risk/base-currency-initial-investment';
|
||||||
import { CurrencyClusterRiskCurrentInvestment } from '../models/rules/currency-cluster-risk/current-investment';
|
import { CurrencyClusterRiskCurrentInvestment } from '../models/rules/currency-cluster-risk/current-investment';
|
||||||
import { CurrencyClusterRiskInitialInvestment } from '../models/rules/currency-cluster-risk/initial-investment';
|
import { CurrencyClusterRiskInitialInvestment } from '../models/rules/currency-cluster-risk/initial-investment';
|
||||||
import { FeeRatioInitialInvestment } from '../models/rules/fees/fee-ratio-initial-investment';
|
import { FeeRatioInitialInvestment } from '../models/rules/fees/fee-ratio-initial-investment';
|
||||||
import { PlatformClusterRiskCurrentInvestment } from '../models/rules/platform-cluster-risk/current-investment';
|
|
||||||
import { PlatformClusterRiskInitialInvestment } from '../models/rules/platform-cluster-risk/initial-investment';
|
|
||||||
import { PlatformClusterRiskSinglePlatform } from '../models/rules/platform-cluster-risk/single-platform';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RulesService {
|
export class RulesService {
|
||||||
@ -39,6 +39,17 @@ export class RulesService {
|
|||||||
|
|
||||||
private getDefaultRuleSettings(aUserSettings: { baseCurrency: string }) {
|
private getDefaultRuleSettings(aUserSettings: { baseCurrency: string }) {
|
||||||
return {
|
return {
|
||||||
|
[AccountClusterRiskCurrentInvestment.name]: {
|
||||||
|
baseCurrency: aUserSettings.baseCurrency,
|
||||||
|
isActive: true,
|
||||||
|
threshold: 0.5
|
||||||
|
},
|
||||||
|
[AccountClusterRiskInitialInvestment.name]: {
|
||||||
|
baseCurrency: aUserSettings.baseCurrency,
|
||||||
|
isActive: true,
|
||||||
|
threshold: 0.5
|
||||||
|
},
|
||||||
|
[AccountClusterRiskSingleAccount.name]: { isActive: true },
|
||||||
[CurrencyClusterRiskBaseCurrencyInitialInvestment.name]: {
|
[CurrencyClusterRiskBaseCurrencyInitialInvestment.name]: {
|
||||||
baseCurrency: aUserSettings.baseCurrency,
|
baseCurrency: aUserSettings.baseCurrency,
|
||||||
isActive: true
|
isActive: true
|
||||||
@ -61,18 +72,7 @@ export class RulesService {
|
|||||||
baseCurrency: aUserSettings.baseCurrency,
|
baseCurrency: aUserSettings.baseCurrency,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
threshold: 0.01
|
threshold: 0.01
|
||||||
},
|
}
|
||||||
[PlatformClusterRiskCurrentInvestment.name]: {
|
|
||||||
baseCurrency: aUserSettings.baseCurrency,
|
|
||||||
isActive: true,
|
|
||||||
threshold: 0.5
|
|
||||||
},
|
|
||||||
[PlatformClusterRiskInitialInvestment.name]: {
|
|
||||||
baseCurrency: aUserSettings.baseCurrency,
|
|
||||||
isActive: true,
|
|
||||||
threshold: 0.5
|
|
||||||
},
|
|
||||||
[PlatformClusterRiskSinglePlatform.name]: { isActive: true }
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,13 +64,25 @@
|
|||||||
<button i18n mat-menu-item (click)="onUpdateAccount(element)">
|
<button i18n mat-menu-item (click)="onUpdateAccount(element)">
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
<button i18n mat-menu-item (click)="onDeleteAccount(element.id)">
|
<button
|
||||||
|
i18n
|
||||||
|
mat-menu-item
|
||||||
|
[disabled]="element.isDefault || element.Order?.length > 0"
|
||||||
|
(click)="onDeleteAccount(element.id)"
|
||||||
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="transactions">
|
||||||
|
<th *matHeaderCellDef i18n mat-header-cell mat-sort-header>Transactions</th>
|
||||||
|
<td *matCellDef="let element" mat-cell>
|
||||||
|
{{ element.Order?.length }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
||||||
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
|
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
|
||||||
</table>
|
</table>
|
||||||
|
@ -13,7 +13,7 @@ import { MatDialog } from '@angular/material/dialog';
|
|||||||
import { MatSort } from '@angular/material/sort';
|
import { MatSort } from '@angular/material/sort';
|
||||||
import { MatTableDataSource } from '@angular/material/table';
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { Order as OrderModel } from '@prisma/client';
|
import { Account as AccountModel } from '@prisma/client';
|
||||||
import { Subject, Subscription } from 'rxjs';
|
import { Subject, Subscription } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -23,18 +23,18 @@ import { Subject, Subscription } from 'rxjs';
|
|||||||
styleUrls: ['./accounts-table.component.scss']
|
styleUrls: ['./accounts-table.component.scss']
|
||||||
})
|
})
|
||||||
export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
|
export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
|
||||||
@Input() accounts: OrderModel[];
|
@Input() accounts: AccountModel[];
|
||||||
@Input() baseCurrency: string;
|
@Input() baseCurrency: string;
|
||||||
@Input() deviceType: string;
|
@Input() deviceType: string;
|
||||||
@Input() locale: string;
|
@Input() locale: string;
|
||||||
@Input() showActions: boolean;
|
@Input() showActions: boolean;
|
||||||
|
|
||||||
@Output() accountDeleted = new EventEmitter<string>();
|
@Output() accountDeleted = new EventEmitter<string>();
|
||||||
@Output() accountToUpdate = new EventEmitter<OrderModel>();
|
@Output() accountToUpdate = new EventEmitter<AccountModel>();
|
||||||
|
|
||||||
@ViewChild(MatSort) sort: MatSort;
|
@ViewChild(MatSort) sort: MatSort;
|
||||||
|
|
||||||
public dataSource: MatTableDataSource<OrderModel> = new MatTableDataSource();
|
public dataSource: MatTableDataSource<AccountModel> = new MatTableDataSource();
|
||||||
public displayedColumns = [];
|
public displayedColumns = [];
|
||||||
public isLoading = true;
|
public isLoading = true;
|
||||||
public routeQueryParams: Subscription;
|
public routeQueryParams: Subscription;
|
||||||
@ -50,14 +50,14 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
public ngOnInit() {}
|
public ngOnInit() {}
|
||||||
|
|
||||||
public ngOnChanges() {
|
public ngOnChanges() {
|
||||||
this.displayedColumns = ['account', 'type', 'platform'];
|
this.displayedColumns = ['account', 'type', 'platform', 'transactions'];
|
||||||
|
|
||||||
this.isLoading = true;
|
|
||||||
|
|
||||||
if (this.showActions) {
|
if (this.showActions) {
|
||||||
this.displayedColumns.push('actions');
|
this.displayedColumns.push('actions');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
if (this.accounts) {
|
if (this.accounts) {
|
||||||
this.dataSource = new MatTableDataSource(this.accounts);
|
this.dataSource = new MatTableDataSource(this.accounts);
|
||||||
this.dataSource.sort = this.sort;
|
this.dataSource.sort = this.sort;
|
||||||
@ -74,7 +74,7 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public onUpdateAccount(aAccount: OrderModel) {
|
public onUpdateAccount(aAccount: AccountModel) {
|
||||||
this.accountToUpdate.emit(aAccount);
|
this.accountToUpdate.emit(aAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,7 +163,7 @@
|
|||||||
i18n
|
i18n
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'account' }"
|
[ngClass]="{ 'font-weight-bold': currentRoute === 'account' }"
|
||||||
>Account</a
|
>Ghostfolio Account</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
*ngIf="hasPermissionToAccessAdminControl"
|
*ngIf="hasPermissionToAccessAdminControl"
|
||||||
|
@ -78,12 +78,12 @@ export class TransactionsTableComponent
|
|||||||
'fee'
|
'fee'
|
||||||
];
|
];
|
||||||
|
|
||||||
this.isLoading = true;
|
|
||||||
|
|
||||||
if (this.showActions) {
|
if (this.showActions) {
|
||||||
this.displayedColumns.push('actions');
|
this.displayedColumns.push('actions');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
if (this.transactions) {
|
if (this.transactions) {
|
||||||
this.dataSource = new MatTableDataSource(this.transactions);
|
this.dataSource = new MatTableDataSource(this.transactions);
|
||||||
this.dataSource.sort = this.sort;
|
this.dataSource.sort = this.sort;
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
|
||||||
|
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
|
||||||
import { User } from '@ghostfolio/api/app/user/interfaces/user.interface';
|
import { 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 { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/helper';
|
import { hasPermission, permissions } from '@ghostfolio/helper';
|
||||||
import { Order as OrderModel } from '@prisma/client';
|
import { Account as AccountModel, AccountType } from '@prisma/client';
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { Subject, Subscription } from 'rxjs';
|
import { Subject, Subscription } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
@ -20,7 +21,7 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog/c
|
|||||||
styleUrls: ['./accounts-page.scss']
|
styleUrls: ['./accounts-page.scss']
|
||||||
})
|
})
|
||||||
export class AccountsPageComponent implements OnInit {
|
export class AccountsPageComponent implements OnInit {
|
||||||
public accounts: OrderModel[];
|
public accounts: AccountModel[];
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
public hasImpersonationId: boolean;
|
public hasImpersonationId: boolean;
|
||||||
public hasPermissionToCreateAccount: boolean;
|
public hasPermissionToCreateAccount: boolean;
|
||||||
@ -116,40 +117,25 @@ export class AccountsPageComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onUpdateAccount(aAccount: OrderModel) {
|
public onUpdateAccount(aAccount: AccountModel) {
|
||||||
this.router.navigate([], {
|
this.router.navigate([], {
|
||||||
queryParams: { editDialog: true, transactionId: aAccount.id }
|
queryParams: { editDialog: true, transactionId: aAccount.id }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public openUpdateAccountDialog({
|
public openUpdateAccountDialog({
|
||||||
accountId,
|
accountType,
|
||||||
currency,
|
|
||||||
dataSource,
|
|
||||||
date,
|
|
||||||
fee,
|
|
||||||
id,
|
id,
|
||||||
platformId,
|
name,
|
||||||
quantity,
|
platformId
|
||||||
symbol,
|
}: AccountModel): void {
|
||||||
type,
|
|
||||||
unitPrice
|
|
||||||
}: OrderModel): void {
|
|
||||||
const dialogRef = this.dialog.open(CreateOrUpdateAccountDialog, {
|
const dialogRef = this.dialog.open(CreateOrUpdateAccountDialog, {
|
||||||
data: {
|
data: {
|
||||||
accounts: this.user.accounts,
|
account: {
|
||||||
transaction: {
|
accountType,
|
||||||
accountId,
|
|
||||||
currency,
|
|
||||||
dataSource,
|
|
||||||
date,
|
|
||||||
fee,
|
|
||||||
id,
|
id,
|
||||||
platformId,
|
name,
|
||||||
quantity,
|
platformId
|
||||||
symbol,
|
|
||||||
type,
|
|
||||||
unitPrice
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||||
@ -157,10 +143,10 @@ export class AccountsPageComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe((data: any) => {
|
dialogRef.afterClosed().subscribe((data: any) => {
|
||||||
const transaction: UpdateOrderDto = data?.transaction;
|
const account: UpdateAccountDto = data?.account;
|
||||||
|
|
||||||
if (transaction) {
|
if (account) {
|
||||||
this.dataService.putAccount(transaction).subscribe({
|
this.dataService.putAccount(account).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.fetchAccounts();
|
this.fetchAccounts();
|
||||||
}
|
}
|
||||||
@ -179,19 +165,10 @@ export class AccountsPageComponent implements OnInit {
|
|||||||
private openCreateAccountDialog(): void {
|
private openCreateAccountDialog(): void {
|
||||||
const dialogRef = this.dialog.open(CreateOrUpdateAccountDialog, {
|
const dialogRef = this.dialog.open(CreateOrUpdateAccountDialog, {
|
||||||
data: {
|
data: {
|
||||||
accounts: this.user?.accounts,
|
account: {
|
||||||
transaction: {
|
accountType: AccountType.SECURITIES,
|
||||||
accountId: this.user?.accounts.find((account) => {
|
name: null,
|
||||||
return account.isDefault;
|
platformId: null
|
||||||
})?.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',
|
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||||
@ -199,10 +176,10 @@ export class AccountsPageComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe((data: any) => {
|
dialogRef.afterClosed().subscribe((data: any) => {
|
||||||
const transaction: UpdateOrderDto = data?.transaction;
|
const account: CreateAccountDto = data?.account;
|
||||||
|
|
||||||
if (transaction) {
|
if (account) {
|
||||||
this.dataService.postAccount(transaction).subscribe({
|
this.dataService.postAccount(account).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.fetchAccounts();
|
this.fetchAccounts();
|
||||||
}
|
}
|
||||||
|
@ -1,47 +1,25 @@
|
|||||||
import {
|
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
|
||||||
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 { 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 { Currency } from '@prisma/client';
|
||||||
import { Observable, Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import {
|
|
||||||
debounceTime,
|
|
||||||
distinctUntilChanged,
|
|
||||||
startWith,
|
|
||||||
switchMap,
|
|
||||||
takeUntil
|
|
||||||
} from 'rxjs/operators';
|
|
||||||
|
|
||||||
import { DataService } from '../../../services/data.service';
|
import { DataService } from '../../../services/data.service';
|
||||||
import { CreateOrUpdateAccountDialogParams } from './interfaces/interfaces';
|
import { CreateOrUpdateAccountDialogParams } from './interfaces/interfaces';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'h-100' },
|
host: { class: 'h-100' },
|
||||||
selector: 'create-or-update-account-dialog',
|
selector: 'gf-create-or-update-account-dialog',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
styleUrls: ['./create-or-update-account-dialog.scss'],
|
styleUrls: ['./create-or-update-account-dialog.scss'],
|
||||||
templateUrl: 'create-or-update-account-dialog.html'
|
templateUrl: 'create-or-update-account-dialog.html'
|
||||||
})
|
})
|
||||||
export class CreateOrUpdateAccountDialog {
|
export class CreateOrUpdateAccountDialog {
|
||||||
public currencies: Currency[] = [];
|
public currencies: Currency[] = [];
|
||||||
public filteredLookupItems: Observable<LookupItem[]>;
|
|
||||||
public isLoading = false;
|
|
||||||
public platforms: { id: string; name: string }[];
|
public platforms: { id: string; name: string }[];
|
||||||
public searchSymbolCtrl = new FormControl(
|
|
||||||
this.data.transaction.symbol,
|
|
||||||
Validators.required
|
|
||||||
);
|
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private cd: ChangeDetectorRef,
|
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
public dialogRef: MatDialogRef<CreateOrUpdateAccountDialog>,
|
public dialogRef: MatDialogRef<CreateOrUpdateAccountDialog>,
|
||||||
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateAccountDialogParams
|
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateAccountDialogParams
|
||||||
@ -52,51 +30,12 @@ export class CreateOrUpdateAccountDialog {
|
|||||||
this.currencies = currencies;
|
this.currencies = currencies;
|
||||||
this.platforms = platforms;
|
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 {
|
public onCancel(): void {
|
||||||
this.dialogRef.close();
|
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() {
|
public ngOnDestroy() {
|
||||||
this.unsubscribeSubject.next();
|
this.unsubscribeSubject.next();
|
||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
|
@ -1,145 +1,25 @@
|
|||||||
<form #addAccountForm="ngForm" class="d-flex flex-column h-100">
|
<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.account.id" mat-dialog-title i18n>Update account</h1>
|
||||||
<h1 *ngIf="!data.transaction.id" mat-dialog-title i18n>Add account</h1>
|
<h1 *ngIf="!data.account.id" mat-dialog-title i18n>Add account</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">
|
||||||
<mat-label i18n>Symbol or ISIN</mat-label>
|
<mat-label i18n>Name</mat-label>
|
||||||
<input
|
<input matInput name="name" required [(ngModel)]="data.account.name" />
|
||||||
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>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<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.transaction.type">
|
<mat-select name="type" required [(value)]="data.account.accountType">
|
||||||
<mat-option value="BUY" i18n> BUY </mat-option>
|
<mat-option value="SECURITIES" i18n> SECURITIES </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-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</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.transaction.platformId">
|
<mat-select name="platformId" [(value)]="data.account.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
|
||||||
@ -154,7 +34,7 @@
|
|||||||
color="primary"
|
color="primary"
|
||||||
i18n
|
i18n
|
||||||
mat-flat-button
|
mat-flat-button
|
||||||
[disabled]="!(addAccountForm.form.valid && data.transaction.symbol)"
|
[disabled]="!addAccountForm.form.valid"
|
||||||
[mat-dialog-close]="data"
|
[mat-dialog-close]="data"
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
|
@ -1,15 +1,11 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
|
||||||
import { MatDialogModule } from '@angular/material/dialog';
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
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 { MatSelectModule } from '@angular/material/select';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
|
||||||
|
|
||||||
import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.component';
|
import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.component';
|
||||||
|
|
||||||
@ -18,15 +14,11 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.c
|
|||||||
exports: [],
|
exports: [],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
GfSymbolModule,
|
|
||||||
FormsModule,
|
FormsModule,
|
||||||
MatAutocompleteModule,
|
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatDatepickerModule,
|
|
||||||
MatDialogModule,
|
MatDialogModule,
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
MatProgressSpinnerModule,
|
|
||||||
MatSelectModule,
|
MatSelectModule,
|
||||||
ReactiveFormsModule
|
ReactiveFormsModule
|
||||||
],
|
],
|
||||||
|
@ -1,9 +1,5 @@
|
|||||||
import { Account } from '@prisma/client';
|
import { Account } from '@prisma/client';
|
||||||
|
|
||||||
import { Order } from '../../interfaces/order.interface';
|
|
||||||
|
|
||||||
export interface CreateOrUpdateAccountDialogParams {
|
export interface CreateOrUpdateAccountDialogParams {
|
||||||
accountId: string;
|
account: Account;
|
||||||
accounts: Account[];
|
|
||||||
transaction: Order;
|
|
||||||
}
|
}
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
import { Currency, DataSource } from '@prisma/client';
|
|
||||||
|
|
||||||
export interface Order {
|
|
||||||
accountId: string;
|
|
||||||
currency: Currency;
|
|
||||||
dataSource: DataSource;
|
|
||||||
date: Date;
|
|
||||||
fee: number;
|
|
||||||
id: string;
|
|
||||||
quantity: number;
|
|
||||||
platformId: string;
|
|
||||||
symbol: string;
|
|
||||||
type: string;
|
|
||||||
unitPrice: number;
|
|
||||||
}
|
|
@ -16,6 +16,9 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
styleUrls: ['./analysis-page.scss']
|
styleUrls: ['./analysis-page.scss']
|
||||||
})
|
})
|
||||||
export class AnalysisPageComponent implements OnDestroy, OnInit {
|
export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||||
|
public accounts: {
|
||||||
|
[symbol: string]: Pick<PortfolioPosition, 'name'> & { value: number };
|
||||||
|
};
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
public period = 'current';
|
public period = 'current';
|
||||||
public periodOptions: ToggleOption[] = [
|
public periodOptions: ToggleOption[] = [
|
||||||
@ -23,9 +26,6 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
{ label: 'Current', value: 'current' }
|
{ label: 'Current', value: 'current' }
|
||||||
];
|
];
|
||||||
public hasImpersonationId: boolean;
|
public hasImpersonationId: boolean;
|
||||||
public platforms: {
|
|
||||||
[symbol: string]: Pick<PortfolioPosition, 'name'> & { value: number };
|
|
||||||
};
|
|
||||||
public portfolioItems: PortfolioItem[];
|
public portfolioItems: PortfolioItem[];
|
||||||
public portfolioPositions: { [symbol: string]: PortfolioPosition };
|
public portfolioPositions: { [symbol: string]: PortfolioPosition };
|
||||||
public positions: { [symbol: string]: any };
|
public positions: { [symbol: string]: any };
|
||||||
@ -95,7 +95,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
},
|
},
|
||||||
aPeriod: string
|
aPeriod: string
|
||||||
) {
|
) {
|
||||||
this.platforms = {};
|
this.accounts = {};
|
||||||
this.positions = {};
|
this.positions = {};
|
||||||
this.positionsArray = [];
|
this.positionsArray = [];
|
||||||
|
|
||||||
@ -113,16 +113,16 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
};
|
};
|
||||||
this.positionsArray.push(position);
|
this.positionsArray.push(position);
|
||||||
|
|
||||||
for (const [platform, { current, original }] of Object.entries(
|
for (const [account, { current, original }] of Object.entries(
|
||||||
position.platforms
|
position.accounts
|
||||||
)) {
|
)) {
|
||||||
if (this.platforms[platform]?.value) {
|
if (this.accounts[account]?.value) {
|
||||||
this.platforms[platform].value +=
|
this.accounts[account].value +=
|
||||||
aPeriod === 'original' ? original : current;
|
aPeriod === 'original' ? original : current;
|
||||||
} else {
|
} else {
|
||||||
this.platforms[platform] = {
|
this.accounts[account] = {
|
||||||
value: aPeriod === 'original' ? original : current,
|
value: aPeriod === 'original' ? original : current,
|
||||||
name: platform
|
name: account
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
<mat-card-header class="w-100">
|
<mat-card-header class="w-100">
|
||||||
<mat-card-title i18n>By Platform</mat-card-title>
|
<mat-card-title i18n>By Account</mat-card-title>
|
||||||
<gf-toggle
|
<gf-toggle
|
||||||
[defaultValue]="period"
|
[defaultValue]="period"
|
||||||
[isLoading]="false"
|
[isLoading]="false"
|
||||||
@ -50,7 +50,7 @@
|
|||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
[isInPercent]="hasImpersonationId"
|
[isInPercent]="hasImpersonationId"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[positions]="platforms"
|
[positions]="accounts"
|
||||||
></gf-portfolio-proportion-chart>
|
></gf-portfolio-proportion-chart>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
@ -10,9 +10,9 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
styleUrls: ['./report-page.scss']
|
styleUrls: ['./report-page.scss']
|
||||||
})
|
})
|
||||||
export class ReportPageComponent implements OnInit {
|
export class ReportPageComponent implements OnInit {
|
||||||
|
public accountClusterRiskRules: PortfolioReportRule[];
|
||||||
public currencyClusterRiskRules: PortfolioReportRule[];
|
public currencyClusterRiskRules: PortfolioReportRule[];
|
||||||
public feeRules: PortfolioReportRule[];
|
public feeRules: PortfolioReportRule[];
|
||||||
public platformClusterRiskRules: PortfolioReportRule[];
|
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
@ -32,11 +32,11 @@ export class ReportPageComponent implements OnInit {
|
|||||||
.fetchPortfolioReport()
|
.fetchPortfolioReport()
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((portfolioReport) => {
|
.subscribe((portfolioReport) => {
|
||||||
|
this.accountClusterRiskRules =
|
||||||
|
portfolioReport.rules['accountClusterRisk'] || null;
|
||||||
this.currencyClusterRiskRules =
|
this.currencyClusterRiskRules =
|
||||||
portfolioReport.rules['currencyClusterRisk'] || null;
|
portfolioReport.rules['currencyClusterRisk'] || null;
|
||||||
this.feeRules = portfolioReport.rules['fees'] || null;
|
this.feeRules = portfolioReport.rules['fees'] || null;
|
||||||
this.platformClusterRiskRules =
|
|
||||||
portfolioReport.rules['platformClusterRisk'] || null;
|
|
||||||
|
|
||||||
this.cd.markForCheck();
|
this.cd.markForCheck();
|
||||||
});
|
});
|
||||||
|
@ -18,8 +18,8 @@
|
|||||||
<gf-rules [rules]="currencyClusterRiskRules"></gf-rules>
|
<gf-rules [rules]="currencyClusterRiskRules"></gf-rules>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h4 class="m-0" i18n>Platform Cluster Risks</h4>
|
<h4 class="m-0" i18n>Account Cluster Risks</h4>
|
||||||
<gf-rules [rules]="platformClusterRiskRules"></gf-rules>
|
<gf-rules [rules]="accountClusterRiskRules"></gf-rules>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 class="m-0" i18n>Fees</h4>
|
<h4 class="m-0" i18n>Fees</h4>
|
||||||
|
@ -121,11 +121,10 @@
|
|||||||
/>
|
/>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-none">
|
<div>
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
<mat-label i18n>Account</mat-label>
|
<mat-label i18n>Account</mat-label>
|
||||||
<mat-select
|
<mat-select
|
||||||
disabled
|
|
||||||
name="accountId"
|
name="accountId"
|
||||||
required
|
required
|
||||||
[(value)]="data.transaction.accountId"
|
[(value)]="data.transaction.accountId"
|
||||||
@ -136,17 +135,6 @@
|
|||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
|
||||||
<mat-label i18n>Platform</mat-label>
|
|
||||||
<mat-select name="platformId" [(value)]="data.transaction.platformId">
|
|
||||||
<mat-option [value]="null"></mat-option>
|
|
||||||
<mat-option *ngFor="let platform of platforms" [value]="platform.id"
|
|
||||||
>{{ platform.name }}</mat-option
|
|
||||||
>
|
|
||||||
</mat-select>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="justify-content-end" mat-dialog-actions>
|
<div class="justify-content-end" mat-dialog-actions>
|
||||||
<button i18n mat-button (click)="onCancel()">Cancel</button>
|
<button i18n mat-button (click)="onCancel()">Cancel</button>
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import { Account } from '@prisma/client';
|
import { Account, Order } from '@prisma/client';
|
||||||
|
|
||||||
import { Order } from '../../interfaces/order.interface';
|
|
||||||
|
|
||||||
export interface CreateOrUpdateTransactionDialogParams {
|
export interface CreateOrUpdateTransactionDialogParams {
|
||||||
accountId: string;
|
accountId: string;
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
import { Currency, DataSource } from '@prisma/client';
|
|
||||||
|
|
||||||
export interface Order {
|
|
||||||
accountId: string;
|
|
||||||
currency: Currency;
|
|
||||||
dataSource: DataSource;
|
|
||||||
date: Date;
|
|
||||||
fee: number;
|
|
||||||
id: string;
|
|
||||||
quantity: number;
|
|
||||||
platformId: string;
|
|
||||||
symbol: string;
|
|
||||||
type: string;
|
|
||||||
unitPrice: number;
|
|
||||||
}
|
|
@ -1,6 +1,7 @@
|
|||||||
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
||||||
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
||||||
import { User } from '@ghostfolio/api/app/user/interfaces/user.interface';
|
import { User } from '@ghostfolio/api/app/user/interfaces/user.interface';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
@ -199,7 +200,7 @@ export class TransactionsPageComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe((data: any) => {
|
dialogRef.afterClosed().subscribe((data: any) => {
|
||||||
const transaction: UpdateOrderDto = data?.transaction;
|
const transaction: CreateOrderDto = data?.transaction;
|
||||||
|
|
||||||
if (transaction) {
|
if (transaction) {
|
||||||
this.dataService.postOrder(transaction).subscribe({
|
this.dataService.postOrder(transaction).subscribe({
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Access } from '@ghostfolio/api/app/access/interfaces/access.interface';
|
import { Access } from '@ghostfolio/api/app/access/interfaces/access.interface';
|
||||||
|
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
|
||||||
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
|
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
|
||||||
import { AdminData } from '@ghostfolio/api/app/admin/interfaces/admin-data.interface';
|
import { AdminData } from '@ghostfolio/api/app/admin/interfaces/admin-data.interface';
|
||||||
import { InfoItem } from '@ghostfolio/api/app/info/interfaces/info-item.interface';
|
import { InfoItem } from '@ghostfolio/api/app/info/interfaces/info-item.interface';
|
||||||
|
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
||||||
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
||||||
import { PortfolioItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-item.interface';
|
import { PortfolioItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-item.interface';
|
||||||
import { PortfolioOverview } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-overview.interface';
|
import { PortfolioOverview } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-overview.interface';
|
||||||
@ -20,6 +22,7 @@ import { UserItem } from '@ghostfolio/api/app/user/interfaces/user-item.interfac
|
|||||||
import { User } from '@ghostfolio/api/app/user/interfaces/user.interface';
|
import { User } from '@ghostfolio/api/app/user/interfaces/user.interface';
|
||||||
import { UpdateUserSettingsDto } from '@ghostfolio/api/app/user/update-user-settings.dto';
|
import { UpdateUserSettingsDto } from '@ghostfolio/api/app/user/update-user-settings.dto';
|
||||||
import { Order as OrderModel } from '@prisma/client';
|
import { Order as OrderModel } from '@prisma/client';
|
||||||
|
import { Account as AccountModel } from '@prisma/client';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@ -30,7 +33,7 @@ export class DataService {
|
|||||||
public constructor(private http: HttpClient) {}
|
public constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
public fetchAccounts() {
|
public fetchAccounts() {
|
||||||
return this.http.get<OrderModel[]>('/api/account');
|
return this.http.get<AccountModel[]>('/api/account');
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchAdminData() {
|
public fetchAdminData() {
|
||||||
@ -117,11 +120,11 @@ 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) {
|
public postAccount(aAccount: CreateAccountDto) {
|
||||||
return this.http.post<OrderModel>(`/api/account`, aAccount);
|
return this.http.post<OrderModel>(`/api/account`, aAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
public postOrder(aOrder: UpdateOrderDto) {
|
public postOrder(aOrder: CreateOrderDto) {
|
||||||
return this.http.post<OrderModel>(`/api/order`, aOrder);
|
return this.http.post<OrderModel>(`/api/order`, aOrder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,9 +32,12 @@ export function getPermissions(aRole: Role): string[] {
|
|||||||
case 'ADMIN':
|
case 'ADMIN':
|
||||||
return [
|
return [
|
||||||
permissions.accessAdminControl,
|
permissions.accessAdminControl,
|
||||||
|
permissions.createAccount,
|
||||||
permissions.createOrder,
|
permissions.createOrder,
|
||||||
|
permissions.deleteAccount,
|
||||||
permissions.deleteOrder,
|
permissions.deleteOrder,
|
||||||
permissions.readForeignPortfolio,
|
permissions.readForeignPortfolio,
|
||||||
|
permissions.updateAccount,
|
||||||
permissions.updateOrder,
|
permissions.updateOrder,
|
||||||
permissions.updateUserSettings
|
permissions.updateUserSettings
|
||||||
];
|
];
|
||||||
@ -44,8 +47,11 @@ export function getPermissions(aRole: Role): string[] {
|
|||||||
|
|
||||||
case 'USER':
|
case 'USER':
|
||||||
return [
|
return [
|
||||||
|
permissions.createAccount,
|
||||||
permissions.createOrder,
|
permissions.createOrder,
|
||||||
|
permissions.deleteAccount,
|
||||||
permissions.deleteOrder,
|
permissions.deleteOrder,
|
||||||
|
permissions.updateAccount,
|
||||||
permissions.updateOrder,
|
permissions.updateOrder,
|
||||||
permissions.updateUserSettings
|
permissions.updateUserSettings
|
||||||
];
|
];
|
||||||
|
Loading…
x
Reference in New Issue
Block a user