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
pull/61/head
Thomas 4 years ago committed by GitHub
parent d17b02092e
commit 90a2fea7d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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() @IsString()
currency: Currency; id: string;
@IsString() @IsString()
dataSource: DataSource; name: string;
@IsISO8601()
date: string;
@IsNumber()
fee: number;
@IsString() @IsString()
@ValidateIf((object, value) => value !== null) @ValidateIf((object, value) => value !== null)
platformId: string | null; platformId: string | null;
@IsString()
id: string;
@IsNumber()
quantity: number;
@IsString()
symbol: string;
@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,32 +396,32 @@ export class Portfolio implements PortfolioInterface {
return { return {
rules: { rules: {
currencyClusterRisk: await this.rulesService.evaluate( accountClusterRisk: await this.rulesService.evaluate(
this, this,
[ [
new CurrencyClusterRiskBaseCurrencyInitialInvestment( new AccountClusterRiskCurrentInvestment(
this.exchangeRateDataService this.exchangeRateDataService
), ),
new CurrencyClusterRiskBaseCurrencyCurrentInvestment( new AccountClusterRiskInitialInvestment(
this.exchangeRateDataService this.exchangeRateDataService
), ),
new CurrencyClusterRiskInitialInvestment( new AccountClusterRiskSingleAccount(this.exchangeRateDataService)
this.exchangeRateDataService
),
new CurrencyClusterRiskCurrentInvestment(
this.exchangeRateDataService
)
], ],
{ baseCurrency: this.user.Settings.currency } { baseCurrency: this.user.Settings.currency }
), ),
platformClusterRisk: await this.rulesService.evaluate( currencyClusterRisk: await this.rulesService.evaluate(
this, this,
[ [
new PlatformClusterRiskSinglePlatform(this.exchangeRateDataService), new CurrencyClusterRiskBaseCurrencyInitialInvestment(
new PlatformClusterRiskInitialInvestment( this.exchangeRateDataService
),
new CurrencyClusterRiskBaseCurrencyCurrentInvestment(
this.exchangeRateDataService this.exchangeRateDataService
), ),
new PlatformClusterRiskCurrentInvestment( new CurrencyClusterRiskInitialInvestment(
this.exchangeRateDataService
),
new CurrencyClusterRiskCurrentInvestment(
this.exchangeRateDataService 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…
Cancel
Save