mirror of https://github.com/ghostfolio/ghostfolio
committed by
GitHub
134 changed files with 9303 additions and 4985 deletions
@ -0,0 +1,249 @@ |
|||
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; |
|||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; |
|||
import { |
|||
activityDummyData, |
|||
loadActivityExportFile, |
|||
symbolProfileDummyData, |
|||
userDummyData |
|||
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; |
|||
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; |
|||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; |
|||
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; |
|||
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; |
|||
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; |
|||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; |
|||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; |
|||
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; |
|||
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; |
|||
import { parseDate } from '@ghostfolio/common/helper'; |
|||
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; |
|||
|
|||
import { Big } from 'big.js'; |
|||
import { join } from 'path'; |
|||
|
|||
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { |
|||
return { |
|||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|||
CurrentRateService: jest.fn().mockImplementation(() => { |
|||
return CurrentRateServiceMock; |
|||
}) |
|||
}; |
|||
}); |
|||
|
|||
jest.mock( |
|||
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', |
|||
() => { |
|||
return { |
|||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|||
PortfolioSnapshotService: jest.fn().mockImplementation(() => { |
|||
return PortfolioSnapshotServiceMock; |
|||
}) |
|||
}; |
|||
} |
|||
); |
|||
|
|||
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { |
|||
return { |
|||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|||
RedisCacheService: jest.fn().mockImplementation(() => { |
|||
return RedisCacheServiceMock; |
|||
}) |
|||
}; |
|||
}); |
|||
|
|||
describe('PortfolioCalculator', () => { |
|||
let activityDtos: CreateOrderDto[]; |
|||
|
|||
let configurationService: ConfigurationService; |
|||
let currentRateService: CurrentRateService; |
|||
let exchangeRateDataService: ExchangeRateDataService; |
|||
let portfolioCalculatorFactory: PortfolioCalculatorFactory; |
|||
let portfolioSnapshotService: PortfolioSnapshotService; |
|||
let redisCacheService: RedisCacheService; |
|||
|
|||
beforeAll(() => { |
|||
activityDtos = loadActivityExportFile( |
|||
join(__dirname, '../../../../../../../test/import/ok-btceur.json') |
|||
); |
|||
}); |
|||
|
|||
beforeEach(() => { |
|||
configurationService = new ConfigurationService(); |
|||
|
|||
currentRateService = new CurrentRateService(null, null, null, null); |
|||
|
|||
exchangeRateDataService = new ExchangeRateDataService( |
|||
null, |
|||
null, |
|||
null, |
|||
null |
|||
); |
|||
|
|||
portfolioSnapshotService = new PortfolioSnapshotService(null); |
|||
|
|||
redisCacheService = new RedisCacheService(null, null); |
|||
|
|||
portfolioCalculatorFactory = new PortfolioCalculatorFactory( |
|||
configurationService, |
|||
currentRateService, |
|||
exchangeRateDataService, |
|||
portfolioSnapshotService, |
|||
redisCacheService, |
|||
null |
|||
); |
|||
}); |
|||
|
|||
describe('get current positions', () => { |
|||
it.only('with BTCUSD buy (in EUR)', async () => { |
|||
jest.useFakeTimers().setSystemTime(parseDate('2022-01-14').getTime()); |
|||
|
|||
const activities: Activity[] = activityDtos.map((activity) => ({ |
|||
...activityDummyData, |
|||
...activity, |
|||
date: parseDate(activity.date), |
|||
feeInAssetProfileCurrency: 4.46, |
|||
SymbolProfile: { |
|||
...symbolProfileDummyData, |
|||
currency: 'USD', |
|||
dataSource: activity.dataSource, |
|||
name: 'Bitcoin', |
|||
symbol: activity.symbol |
|||
}, |
|||
unitPriceInAssetProfileCurrency: 44558.42 |
|||
})); |
|||
|
|||
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ |
|||
activities, |
|||
calculationType: PerformanceCalculationType.ROAI, |
|||
currency: 'USD', |
|||
userId: userDummyData.id |
|||
}); |
|||
|
|||
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); |
|||
|
|||
const investments = portfolioCalculator.getInvestments(); |
|||
|
|||
const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ |
|||
data: portfolioSnapshot.historicalData, |
|||
groupBy: 'month' |
|||
}); |
|||
|
|||
expect(portfolioSnapshot.historicalData[0]).toEqual({ |
|||
date: '2021-12-11', |
|||
investmentValueWithCurrencyEffect: 0, |
|||
netPerformance: 0, |
|||
netPerformanceInPercentage: 0, |
|||
netPerformanceInPercentageWithCurrencyEffect: 0, |
|||
netPerformanceWithCurrencyEffect: 0, |
|||
netWorth: 0, |
|||
timeWeightedPerformanceInPercentage: 0, |
|||
timeWeightedPerformanceInPercentageWithCurrencyEffect: 0, |
|||
totalAccountBalance: 0, |
|||
totalInvestment: 0, |
|||
totalInvestmentValueWithCurrencyEffect: 0, |
|||
value: 0, |
|||
valueWithCurrencyEffect: 0 |
|||
}); |
|||
|
|||
/** |
|||
* Closing price on 2021-12-12: 50098.3 |
|||
*/ |
|||
expect(portfolioSnapshot.historicalData[1]).toEqual({ |
|||
date: '2021-12-12', |
|||
investmentValueWithCurrencyEffect: 44558.42, |
|||
netPerformance: 5535.42, // 1 * (50098.3 - 44558.42) - 4.46 = 5535.42
|
|||
netPerformanceInPercentage: 0.12422837255001412, // 5535.42 ÷ 44558.42 = 0.12422837255001412
|
|||
netPerformanceInPercentageWithCurrencyEffect: 0.12422837255001412, // 5535.42 ÷ 44558.42 = 0.12422837255001412
|
|||
netPerformanceWithCurrencyEffect: 5535.42, |
|||
netWorth: 50098.3, // 1 * 50098.3 = 50098.3
|
|||
timeWeightedPerformanceInPercentage: 0, |
|||
timeWeightedPerformanceInPercentageWithCurrencyEffect: 0, |
|||
totalAccountBalance: 0, |
|||
totalInvestment: 44558.42, |
|||
totalInvestmentValueWithCurrencyEffect: 44558.42, |
|||
value: 50098.3, // 1 * 50098.3 = 50098.3
|
|||
valueWithCurrencyEffect: 50098.3 |
|||
}); |
|||
|
|||
expect( |
|||
portfolioSnapshot.historicalData[ |
|||
portfolioSnapshot.historicalData.length - 1 |
|||
] |
|||
).toEqual({ |
|||
date: '2022-01-14', |
|||
investmentValueWithCurrencyEffect: 0, |
|||
netPerformance: -1463.18, |
|||
netPerformanceInPercentage: -0.032837340282712, |
|||
netPerformanceInPercentageWithCurrencyEffect: -0.032837340282712, |
|||
netPerformanceWithCurrencyEffect: -1463.18, |
|||
netWorth: 43099.7, |
|||
timeWeightedPerformanceInPercentage: -0.13969735500006986, |
|||
timeWeightedPerformanceInPercentageWithCurrencyEffect: |
|||
-0.13969735500006986, |
|||
totalAccountBalance: 0, |
|||
totalInvestment: 44558.42, |
|||
totalInvestmentValueWithCurrencyEffect: 44558.42, |
|||
value: 43099.7, |
|||
valueWithCurrencyEffect: 43099.7 |
|||
}); |
|||
|
|||
expect(portfolioSnapshot).toMatchObject({ |
|||
currentValueInBaseCurrency: new Big('43099.7'), |
|||
errors: [], |
|||
hasErrors: false, |
|||
positions: [ |
|||
{ |
|||
averagePrice: new Big('44558.42'), |
|||
currency: 'USD', |
|||
dataSource: 'YAHOO', |
|||
dividend: new Big('0'), |
|||
dividendInBaseCurrency: new Big('0'), |
|||
fee: new Big('4.46'), |
|||
feeInBaseCurrency: new Big('4.46'), |
|||
firstBuyDate: '2021-12-12', |
|||
grossPerformance: new Big('-1458.72'), |
|||
grossPerformancePercentage: new Big('-0.03273724696701543726'), |
|||
grossPerformancePercentageWithCurrencyEffect: new Big( |
|||
'-0.03273724696701543726' |
|||
), |
|||
grossPerformanceWithCurrencyEffect: new Big('-1458.72'), |
|||
investment: new Big('44558.42'), |
|||
investmentWithCurrencyEffect: new Big('44558.42'), |
|||
netPerformance: new Big('-1463.18'), |
|||
netPerformancePercentage: new Big('-0.03283734028271199921'), |
|||
netPerformancePercentageWithCurrencyEffectMap: { |
|||
max: new Big('-0.03283734028271199921') |
|||
}, |
|||
netPerformanceWithCurrencyEffectMap: { |
|||
max: new Big('-1463.18') |
|||
}, |
|||
marketPrice: 43099.7, |
|||
marketPriceInBaseCurrency: 43099.7, |
|||
quantity: new Big('1'), |
|||
symbol: 'BTCUSD', |
|||
tags: [], |
|||
timeWeightedInvestment: new Big('44558.42'), |
|||
timeWeightedInvestmentWithCurrencyEffect: new Big('44558.42'), |
|||
transactionCount: 1, |
|||
valueInBaseCurrency: new Big('43099.7') |
|||
} |
|||
], |
|||
totalFeesWithCurrencyEffect: new Big('4.46'), |
|||
totalInterestWithCurrencyEffect: new Big('0'), |
|||
totalInvestment: new Big('44558.42'), |
|||
totalInvestmentWithCurrencyEffect: new Big('44558.42'), |
|||
totalLiabilitiesWithCurrencyEffect: new Big('0'), |
|||
totalValuablesWithCurrencyEffect: new Big('0') |
|||
}); |
|||
|
|||
expect(investments).toEqual([ |
|||
{ date: '2021-12-12', investment: new Big('44558.42') } |
|||
]); |
|||
|
|||
expect(investmentsByMonth).toEqual([ |
|||
{ date: '2021-12-01', investment: 44558.42 }, |
|||
{ date: '2022-01-01', investment: 0 } |
|||
]); |
|||
}); |
|||
}); |
|||
}); |
@ -0,0 +1,253 @@ |
|||
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; |
|||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; |
|||
import { OrderService } from '@ghostfolio/api/app/order/order.service'; |
|||
import { |
|||
activityDummyData, |
|||
loadActivityExportFile, |
|||
symbolProfileDummyData, |
|||
userDummyData |
|||
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; |
|||
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; |
|||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; |
|||
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; |
|||
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; |
|||
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; |
|||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; |
|||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; |
|||
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; |
|||
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; |
|||
import { parseDate } from '@ghostfolio/common/helper'; |
|||
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; |
|||
|
|||
import { Big } from 'big.js'; |
|||
import { join } from 'path'; |
|||
|
|||
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { |
|||
return { |
|||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|||
CurrentRateService: jest.fn().mockImplementation(() => { |
|||
return CurrentRateServiceMock; |
|||
}) |
|||
}; |
|||
}); |
|||
|
|||
jest.mock( |
|||
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', |
|||
() => { |
|||
return { |
|||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|||
PortfolioSnapshotService: jest.fn().mockImplementation(() => { |
|||
return PortfolioSnapshotServiceMock; |
|||
}) |
|||
}; |
|||
} |
|||
); |
|||
|
|||
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { |
|||
return { |
|||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|||
RedisCacheService: jest.fn().mockImplementation(() => { |
|||
return RedisCacheServiceMock; |
|||
}) |
|||
}; |
|||
}); |
|||
|
|||
describe('PortfolioCalculator', () => { |
|||
let activityDtos: CreateOrderDto[]; |
|||
|
|||
let configurationService: ConfigurationService; |
|||
let currentRateService: CurrentRateService; |
|||
let exchangeRateDataService: ExchangeRateDataService; |
|||
let portfolioCalculatorFactory: PortfolioCalculatorFactory; |
|||
let portfolioSnapshotService: PortfolioSnapshotService; |
|||
let redisCacheService: RedisCacheService; |
|||
let orderService: OrderService; |
|||
|
|||
beforeAll(() => { |
|||
activityDtos = loadActivityExportFile( |
|||
join(__dirname, '../../../../../../../test/import/ok-btcusd.json') |
|||
); |
|||
}); |
|||
|
|||
beforeEach(() => { |
|||
configurationService = new ConfigurationService(); |
|||
|
|||
currentRateService = new CurrentRateService(null, null, null, null); |
|||
|
|||
exchangeRateDataService = new ExchangeRateDataService( |
|||
null, |
|||
null, |
|||
null, |
|||
null |
|||
); |
|||
|
|||
portfolioSnapshotService = new PortfolioSnapshotService(null); |
|||
|
|||
redisCacheService = new RedisCacheService(null, null); |
|||
|
|||
orderService = new OrderService(null, null, null, null, null, null); |
|||
|
|||
portfolioCalculatorFactory = new PortfolioCalculatorFactory( |
|||
configurationService, |
|||
currentRateService, |
|||
exchangeRateDataService, |
|||
portfolioSnapshotService, |
|||
redisCacheService, |
|||
orderService |
|||
); |
|||
}); |
|||
|
|||
describe('get current positions', () => { |
|||
it.only('with BTCUSD buy (in USD)', async () => { |
|||
jest.useFakeTimers().setSystemTime(parseDate('2022-01-14').getTime()); |
|||
|
|||
const activities: Activity[] = activityDtos.map((activity) => ({ |
|||
...activityDummyData, |
|||
...activity, |
|||
date: parseDate(activity.date), |
|||
feeInAssetProfileCurrency: 4.46, |
|||
SymbolProfile: { |
|||
...symbolProfileDummyData, |
|||
currency: 'USD', |
|||
dataSource: activity.dataSource, |
|||
name: 'Bitcoin', |
|||
symbol: activity.symbol |
|||
}, |
|||
unitPriceInAssetProfileCurrency: 44558.42 |
|||
})); |
|||
|
|||
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ |
|||
activities, |
|||
calculationType: PerformanceCalculationType.ROAI, |
|||
currency: 'USD', |
|||
userId: userDummyData.id |
|||
}); |
|||
|
|||
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); |
|||
|
|||
const investments = portfolioCalculator.getInvestments(); |
|||
|
|||
const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ |
|||
data: portfolioSnapshot.historicalData, |
|||
groupBy: 'month' |
|||
}); |
|||
|
|||
expect(portfolioSnapshot.historicalData[0]).toEqual({ |
|||
date: '2021-12-11', |
|||
investmentValueWithCurrencyEffect: 0, |
|||
netPerformance: 0, |
|||
netPerformanceInPercentage: 0, |
|||
netPerformanceInPercentageWithCurrencyEffect: 0, |
|||
netPerformanceWithCurrencyEffect: 0, |
|||
netWorth: 0, |
|||
timeWeightedPerformanceInPercentage: 0, |
|||
timeWeightedPerformanceInPercentageWithCurrencyEffect: 0, |
|||
totalAccountBalance: 0, |
|||
totalInvestment: 0, |
|||
totalInvestmentValueWithCurrencyEffect: 0, |
|||
value: 0, |
|||
valueWithCurrencyEffect: 0 |
|||
}); |
|||
|
|||
/** |
|||
* Closing price on 2021-12-12: 50098.3 |
|||
*/ |
|||
expect(portfolioSnapshot.historicalData[1]).toEqual({ |
|||
date: '2021-12-12', |
|||
investmentValueWithCurrencyEffect: 44558.42, |
|||
netPerformance: 5535.42, // 1 * (50098.3 - 44558.42) - 4.46 = 5535.42
|
|||
netPerformanceInPercentage: 0.12422837255001412, // 5535.42 ÷ 44558.42 = 0.12422837255001412
|
|||
netPerformanceInPercentageWithCurrencyEffect: 0.12422837255001412, // 5535.42 ÷ 44558.42 = 0.12422837255001412
|
|||
netPerformanceWithCurrencyEffect: 5535.42, // 1 * (50098.3 - 44558.42) - 4.46 = 5535.42
|
|||
netWorth: 50098.3, // 1 * 50098.3 = 50098.3
|
|||
timeWeightedPerformanceInPercentage: 0, |
|||
timeWeightedPerformanceInPercentageWithCurrencyEffect: 0, |
|||
totalAccountBalance: 0, |
|||
totalInvestment: 44558.42, |
|||
totalInvestmentValueWithCurrencyEffect: 44558.42, |
|||
value: 50098.3, // 1 * 50098.3 = 50098.3
|
|||
valueWithCurrencyEffect: 50098.3 |
|||
}); |
|||
|
|||
expect( |
|||
portfolioSnapshot.historicalData[ |
|||
portfolioSnapshot.historicalData.length - 1 |
|||
] |
|||
).toEqual({ |
|||
date: '2022-01-14', |
|||
investmentValueWithCurrencyEffect: 0, |
|||
netPerformance: -1463.18, |
|||
netPerformanceInPercentage: -0.032837340282712, |
|||
netPerformanceInPercentageWithCurrencyEffect: -0.032837340282712, |
|||
netPerformanceWithCurrencyEffect: -1463.18, |
|||
netWorth: 43099.7, |
|||
timeWeightedPerformanceInPercentage: -0.13969735500006986, |
|||
timeWeightedPerformanceInPercentageWithCurrencyEffect: |
|||
-0.13969735500006986, |
|||
totalAccountBalance: 0, |
|||
totalInvestment: 44558.42, |
|||
totalInvestmentValueWithCurrencyEffect: 44558.42, |
|||
value: 43099.7, |
|||
valueWithCurrencyEffect: 43099.7 |
|||
}); |
|||
|
|||
expect(portfolioSnapshot).toMatchObject({ |
|||
currentValueInBaseCurrency: new Big('43099.7'), |
|||
errors: [], |
|||
hasErrors: false, |
|||
positions: [ |
|||
{ |
|||
averagePrice: new Big('44558.42'), |
|||
currency: 'USD', |
|||
dataSource: 'YAHOO', |
|||
dividend: new Big('0'), |
|||
dividendInBaseCurrency: new Big('0'), |
|||
fee: new Big('4.46'), |
|||
feeInBaseCurrency: new Big('4.46'), |
|||
firstBuyDate: '2021-12-12', |
|||
grossPerformance: new Big('-1458.72'), |
|||
grossPerformancePercentage: new Big('-0.03273724696701543726'), |
|||
grossPerformancePercentageWithCurrencyEffect: new Big( |
|||
'-0.03273724696701543726' |
|||
), |
|||
grossPerformanceWithCurrencyEffect: new Big('-1458.72'), |
|||
investment: new Big('44558.42'), |
|||
investmentWithCurrencyEffect: new Big('44558.42'), |
|||
netPerformance: new Big('-1463.18'), |
|||
netPerformancePercentage: new Big('-0.03283734028271199921'), |
|||
netPerformancePercentageWithCurrencyEffectMap: { |
|||
max: new Big('-0.03283734028271199921') |
|||
}, |
|||
netPerformanceWithCurrencyEffectMap: { |
|||
max: new Big('-1463.18') |
|||
}, |
|||
marketPrice: 43099.7, |
|||
marketPriceInBaseCurrency: 43099.7, |
|||
quantity: new Big('1'), |
|||
symbol: 'BTCUSD', |
|||
tags: [], |
|||
timeWeightedInvestment: new Big('44558.42'), |
|||
timeWeightedInvestmentWithCurrencyEffect: new Big('44558.42'), |
|||
transactionCount: 1, |
|||
valueInBaseCurrency: new Big('43099.7') |
|||
} |
|||
], |
|||
totalFeesWithCurrencyEffect: new Big('4.46'), |
|||
totalInterestWithCurrencyEffect: new Big('0'), |
|||
totalInvestment: new Big('44558.42'), |
|||
totalInvestmentWithCurrencyEffect: new Big('44558.42'), |
|||
totalLiabilitiesWithCurrencyEffect: new Big('0'), |
|||
totalValuablesWithCurrencyEffect: new Big('0') |
|||
}); |
|||
|
|||
expect(investments).toEqual([ |
|||
{ date: '2021-12-12', investment: new Big('44558.42') } |
|||
]); |
|||
|
|||
expect(investmentsByMonth).toEqual([ |
|||
{ date: '2021-12-01', investment: 44558.42 }, |
|||
{ date: '2022-01-01', investment: 0 } |
|||
]); |
|||
}); |
|||
}); |
|||
}); |
@ -1,23 +0,0 @@ |
|||
import { PortfolioItem, Position } from '@ghostfolio/common/interfaces'; |
|||
|
|||
import { Order } from '../order'; |
|||
|
|||
export interface PortfolioInterface { |
|||
get(aDate?: Date): PortfolioItem[]; |
|||
|
|||
getFees(): number; |
|||
|
|||
getPositions(aDate: Date): { |
|||
[symbol: string]: Position; |
|||
}; |
|||
|
|||
getSymbols(aDate?: Date): string[]; |
|||
|
|||
getTotalBuy(): number; |
|||
|
|||
getTotalSell(): number; |
|||
|
|||
getOrders(): Order[]; |
|||
|
|||
getValue(aDate?: Date): number; |
|||
} |
@ -1,83 +0,0 @@ |
|||
import { IOrder } from '@ghostfolio/api/services/interfaces/interfaces'; |
|||
|
|||
import { Account, SymbolProfile, Type as ActivityType } from '@prisma/client'; |
|||
import { v4 as uuidv4 } from 'uuid'; |
|||
|
|||
export class Order { |
|||
private account: Account; |
|||
private currency: string; |
|||
private fee: number; |
|||
private date: string; |
|||
private id: string; |
|||
private isDraft: boolean; |
|||
private quantity: number; |
|||
private symbol: string; |
|||
private symbolProfile: SymbolProfile; |
|||
private total: number; |
|||
private type: ActivityType; |
|||
private unitPrice: number; |
|||
|
|||
public constructor(data: IOrder) { |
|||
this.account = data.account; |
|||
this.currency = data.currency; |
|||
this.fee = data.fee; |
|||
this.date = data.date; |
|||
this.id = data.id || uuidv4(); |
|||
this.isDraft = data.isDraft; |
|||
this.quantity = data.quantity; |
|||
this.symbol = data.symbol; |
|||
this.symbolProfile = data.symbolProfile; |
|||
this.type = data.type; |
|||
this.unitPrice = data.unitPrice; |
|||
|
|||
this.total = this.quantity * data.unitPrice; |
|||
} |
|||
|
|||
public getAccount() { |
|||
return this.account; |
|||
} |
|||
|
|||
public getCurrency() { |
|||
return this.currency; |
|||
} |
|||
|
|||
public getDate() { |
|||
return this.date; |
|||
} |
|||
|
|||
public getFee() { |
|||
return this.fee; |
|||
} |
|||
|
|||
public getId() { |
|||
return this.id; |
|||
} |
|||
|
|||
public getIsDraft() { |
|||
return this.isDraft; |
|||
} |
|||
|
|||
public getQuantity() { |
|||
return this.quantity; |
|||
} |
|||
|
|||
public getSymbol() { |
|||
return this.symbol; |
|||
} |
|||
|
|||
getSymbolProfile() { |
|||
return this.symbolProfile; |
|||
} |
|||
|
|||
public getTotal() { |
|||
return this.total; |
|||
} |
|||
|
|||
public getType() { |
|||
return this.type; |
|||
} |
|||
|
|||
public getUnitPrice() { |
|||
return this.unitPrice; |
|||
} |
|||
} |
@ -1,115 +1,94 @@ |
|||
<div class="container"> |
|||
<div class="row"> |
|||
<div class="col"> |
|||
<div class="d-flex justify-content-end"> |
|||
<a |
|||
color="primary" |
|||
i18n |
|||
mat-flat-button |
|||
[queryParams]="{ createPlatformDialog: true }" |
|||
[routerLink]="[]" |
|||
> |
|||
Add Platform |
|||
</a> |
|||
</div> |
|||
<table |
|||
class="gf-table w-100" |
|||
mat-table |
|||
matSort |
|||
matSortActive="name" |
|||
matSortDirection="asc" |
|||
[dataSource]="dataSource" |
|||
> |
|||
<ng-container matColumnDef="name"> |
|||
<th |
|||
*matHeaderCellDef |
|||
class="px-1" |
|||
mat-header-cell |
|||
mat-sort-header="name" |
|||
> |
|||
<ng-container i18n>Name</ng-container> |
|||
</th> |
|||
<td *matCellDef="let element" class="px-1" mat-cell> |
|||
@if (element.url) { |
|||
<gf-asset-profile-icon |
|||
class="d-inline mr-1" |
|||
[tooltip]="element.name" |
|||
[url]="element.url" |
|||
/> |
|||
} |
|||
<span>{{ element.name }}</span> |
|||
</td></ng-container |
|||
> |
|||
<div class="d-flex justify-content-end"> |
|||
<a |
|||
color="primary" |
|||
i18n |
|||
mat-flat-button |
|||
[queryParams]="{ createPlatformDialog: true }" |
|||
[routerLink]="[]" |
|||
> |
|||
Add Platform |
|||
</a> |
|||
</div> |
|||
<table |
|||
class="gf-table w-100" |
|||
mat-table |
|||
matSort |
|||
matSortActive="name" |
|||
matSortDirection="asc" |
|||
[dataSource]="dataSource" |
|||
> |
|||
<ng-container matColumnDef="name"> |
|||
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header="name"> |
|||
<ng-container i18n>Name</ng-container> |
|||
</th> |
|||
<td *matCellDef="let element" class="px-1" mat-cell> |
|||
@if (element.url) { |
|||
<gf-asset-profile-icon |
|||
class="d-inline mr-1" |
|||
[tooltip]="element.name" |
|||
[url]="element.url" |
|||
/> |
|||
} |
|||
<span>{{ element.name }}</span> |
|||
</td></ng-container |
|||
> |
|||
|
|||
<ng-container matColumnDef="url"> |
|||
<th |
|||
*matHeaderCellDef |
|||
class="px-1" |
|||
mat-header-cell |
|||
mat-sort-header="url" |
|||
> |
|||
<ng-container i18n>Url</ng-container> |
|||
</th> |
|||
<td *matCellDef="let element" class="px-1" mat-cell> |
|||
{{ element.url }} |
|||
</td> |
|||
</ng-container> |
|||
<ng-container matColumnDef="url"> |
|||
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header="url"> |
|||
<ng-container i18n>Url</ng-container> |
|||
</th> |
|||
<td *matCellDef="let element" class="px-1" mat-cell> |
|||
{{ element.url }} |
|||
</td> |
|||
</ng-container> |
|||
|
|||
<ng-container matColumnDef="accounts"> |
|||
<th |
|||
*matHeaderCellDef |
|||
class="px-1" |
|||
mat-header-cell |
|||
mat-sort-header="accountCount" |
|||
> |
|||
<ng-container i18n>Accounts</ng-container> |
|||
</th> |
|||
<td *matCellDef="let element" class="px-1" mat-cell> |
|||
{{ element.accountCount }} |
|||
</td> |
|||
</ng-container> |
|||
<ng-container matColumnDef="accounts"> |
|||
<th |
|||
*matHeaderCellDef |
|||
class="px-1" |
|||
mat-header-cell |
|||
mat-sort-header="accountCount" |
|||
> |
|||
<ng-container i18n>Accounts</ng-container> |
|||
</th> |
|||
<td *matCellDef="let element" class="px-1 text-right" mat-cell> |
|||
{{ element.accountCount }} |
|||
</td> |
|||
</ng-container> |
|||
|
|||
<ng-container matColumnDef="actions" stickyEnd> |
|||
<th |
|||
*matHeaderCellDef |
|||
class="px-1 text-center" |
|||
i18n |
|||
mat-header-cell |
|||
></th> |
|||
<td *matCellDef="let element" class="px-1 text-center" mat-cell> |
|||
<button |
|||
class="mx-1 no-min-width px-2" |
|||
mat-button |
|||
[matMenuTriggerFor]="platformMenu" |
|||
(click)="$event.stopPropagation()" |
|||
> |
|||
<ion-icon name="ellipsis-horizontal" /> |
|||
</button> |
|||
<mat-menu #platformMenu="matMenu" xPosition="before"> |
|||
<button mat-menu-item (click)="onUpdatePlatform(element)"> |
|||
<span class="align-items-center d-flex"> |
|||
<ion-icon class="mr-2" name="create-outline" /> |
|||
<span i18n>Edit</span> |
|||
</span> |
|||
</button> |
|||
<hr class="m-0" /> |
|||
<button |
|||
mat-menu-item |
|||
[disabled]="element.accountCount > 0" |
|||
(click)="onDeletePlatform(element.id)" |
|||
> |
|||
<span class="align-items-center d-flex"> |
|||
<ion-icon class="mr-2" name="trash-outline" /> |
|||
<span i18n>Delete</span> |
|||
</span> |
|||
</button> |
|||
</mat-menu> |
|||
</td> |
|||
</ng-container> |
|||
<ng-container matColumnDef="actions" stickyEnd> |
|||
<th *matHeaderCellDef class="px-1 text-center" i18n mat-header-cell></th> |
|||
<td *matCellDef="let element" class="px-1 text-center" mat-cell> |
|||
<button |
|||
class="mx-1 no-min-width px-2" |
|||
mat-button |
|||
[matMenuTriggerFor]="platformMenu" |
|||
(click)="$event.stopPropagation()" |
|||
> |
|||
<ion-icon name="ellipsis-horizontal" /> |
|||
</button> |
|||
<mat-menu #platformMenu="matMenu" xPosition="before"> |
|||
<button mat-menu-item (click)="onUpdatePlatform(element)"> |
|||
<span class="align-items-center d-flex"> |
|||
<ion-icon class="mr-2" name="create-outline" /> |
|||
<span i18n>Edit</span> |
|||
</span> |
|||
</button> |
|||
<hr class="m-0" /> |
|||
<button |
|||
mat-menu-item |
|||
[disabled]="element.accountCount > 0" |
|||
(click)="onDeletePlatform(element.id)" |
|||
> |
|||
<span class="align-items-center d-flex"> |
|||
<ion-icon class="mr-2" name="trash-outline" /> |
|||
<span i18n>Delete</span> |
|||
</span> |
|||
</button> |
|||
</mat-menu> |
|||
</td> |
|||
</ng-container> |
|||
|
|||
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr> |
|||
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr> |
|||
</table> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr> |
|||
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr> |
|||
</table> |
|||
|
@ -1,3 +1,15 @@ |
|||
:host { |
|||
display: block; |
|||
|
|||
.mat-mdc-progress-bar { |
|||
--mdc-linear-progress-active-indicator-height: 0.5rem; |
|||
--mdc-linear-progress-track-height: 0.5rem; |
|||
border-radius: 0.25rem; |
|||
|
|||
::ng-deep { |
|||
.mdc-linear-progress__buffer-bar { |
|||
background-color: rgb(var(--palette-background-unselected-chip)); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
@ -1,7 +1,4 @@ |
|||
import { User } from '@ghostfolio/common/interfaces'; |
|||
|
|||
export interface GhostfolioPremiumApiDialogParams { |
|||
deviceType: string; |
|||
pricingUrl: string; |
|||
user: User; |
|||
} |
|||
|
@ -1,121 +1,100 @@ |
|||
<div class="container"> |
|||
<div class="row"> |
|||
<div class="col"> |
|||
<div class="d-flex justify-content-end"> |
|||
<a |
|||
color="primary" |
|||
i18n |
|||
mat-flat-button |
|||
[queryParams]="{ createTagDialog: true }" |
|||
[routerLink]="[]" |
|||
> |
|||
Add Tag |
|||
</a> |
|||
</div> |
|||
<table |
|||
class="gf-table w-100" |
|||
mat-table |
|||
matSort |
|||
matSortActive="name" |
|||
matSortDirection="asc" |
|||
[dataSource]="dataSource" |
|||
> |
|||
<ng-container matColumnDef="name"> |
|||
<th |
|||
*matHeaderCellDef |
|||
class="px-1" |
|||
mat-header-cell |
|||
mat-sort-header="name" |
|||
> |
|||
<ng-container i18n>Name</ng-container> |
|||
</th> |
|||
<td *matCellDef="let element" class="px-1" mat-cell> |
|||
{{ element.name }} |
|||
</td> |
|||
</ng-container> |
|||
<div class="d-flex justify-content-end"> |
|||
<a |
|||
color="primary" |
|||
i18n |
|||
mat-flat-button |
|||
[queryParams]="{ createTagDialog: true }" |
|||
[routerLink]="[]" |
|||
> |
|||
Add Tag |
|||
</a> |
|||
</div> |
|||
<table |
|||
class="gf-table w-100" |
|||
mat-table |
|||
matSort |
|||
matSortActive="name" |
|||
matSortDirection="asc" |
|||
[dataSource]="dataSource" |
|||
> |
|||
<ng-container matColumnDef="name"> |
|||
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header="name"> |
|||
<ng-container i18n>Name</ng-container> |
|||
</th> |
|||
<td *matCellDef="let element" class="px-1" mat-cell> |
|||
{{ element.name }} |
|||
</td> |
|||
</ng-container> |
|||
|
|||
<ng-container matColumnDef="userId"> |
|||
<th |
|||
*matHeaderCellDef |
|||
class="px-1" |
|||
mat-header-cell |
|||
mat-sort-header="userId" |
|||
> |
|||
<ng-container i18n>User</ng-container> |
|||
</th> |
|||
<td *matCellDef="let element" class="px-1" mat-cell> |
|||
<span class="text-monospace">{{ element.userId }}</span> |
|||
</td> |
|||
</ng-container> |
|||
<ng-container matColumnDef="userId"> |
|||
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header="userId"> |
|||
<ng-container i18n>User</ng-container> |
|||
</th> |
|||
<td *matCellDef="let element" class="px-1" mat-cell> |
|||
<span class="text-monospace">{{ element.userId }}</span> |
|||
</td> |
|||
</ng-container> |
|||
|
|||
<ng-container matColumnDef="activities"> |
|||
<th |
|||
*matHeaderCellDef |
|||
class="px-1" |
|||
mat-header-cell |
|||
mat-sort-header="activityCount" |
|||
> |
|||
<ng-container i18n>Activities</ng-container> |
|||
</th> |
|||
<td *matCellDef="let element" class="px-1" mat-cell> |
|||
{{ element.activityCount }} |
|||
</td> |
|||
</ng-container> |
|||
<ng-container matColumnDef="holdings"> |
|||
<th |
|||
*matHeaderCellDef |
|||
class="px-1" |
|||
mat-header-cell |
|||
mat-sort-header="holdingCount" |
|||
> |
|||
<ng-container i18n>Holdings</ng-container> |
|||
</th> |
|||
<td *matCellDef="let element" class="px-1" mat-cell> |
|||
{{ element.holdingCount }} |
|||
</td> |
|||
</ng-container> |
|||
<ng-container matColumnDef="activities"> |
|||
<th |
|||
*matHeaderCellDef |
|||
class="px-1" |
|||
mat-header-cell |
|||
mat-sort-header="activityCount" |
|||
> |
|||
<ng-container i18n>Activities</ng-container> |
|||
</th> |
|||
<td *matCellDef="let element" class="px-1 text-right" mat-cell> |
|||
{{ element.activityCount }} |
|||
</td> |
|||
</ng-container> |
|||
<ng-container matColumnDef="holdings"> |
|||
<th |
|||
*matHeaderCellDef |
|||
class="px-1" |
|||
mat-header-cell |
|||
mat-sort-header="holdingCount" |
|||
> |
|||
<ng-container i18n>Holdings</ng-container> |
|||
</th> |
|||
<td *matCellDef="let element" class="px-1" mat-cell> |
|||
{{ element.holdingCount }} |
|||
</td> |
|||
</ng-container> |
|||
|
|||
<ng-container matColumnDef="actions" stickyEnd> |
|||
<th |
|||
*matHeaderCellDef |
|||
class="px-1 text-center" |
|||
i18n |
|||
mat-header-cell |
|||
></th> |
|||
<td *matCellDef="let element" class="px-1 text-center" mat-cell> |
|||
<button |
|||
class="mx-1 no-min-width px-2" |
|||
mat-button |
|||
[matMenuTriggerFor]="tagMenu" |
|||
(click)="$event.stopPropagation()" |
|||
> |
|||
<ion-icon name="ellipsis-horizontal" /> |
|||
</button> |
|||
<mat-menu #tagMenu="matMenu" xPosition="before"> |
|||
<button mat-menu-item (click)="onUpdateTag(element)"> |
|||
<span class="align-items-center d-flex"> |
|||
<ion-icon class="mr-2" name="create-outline" /> |
|||
<span i18n>Edit</span> |
|||
</span> |
|||
</button> |
|||
<hr class="m-0" /> |
|||
<button |
|||
mat-menu-item |
|||
[disabled]="element.activityCount > 0" |
|||
(click)="onDeleteTag(element.id)" |
|||
> |
|||
<span class="align-items-center d-flex"> |
|||
<ion-icon class="mr-2" name="trash-outline" /> |
|||
<span i18n>Delete</span> |
|||
</span> |
|||
</button> |
|||
</mat-menu> |
|||
</td> |
|||
</ng-container> |
|||
<ng-container matColumnDef="actions" stickyEnd> |
|||
<th *matHeaderCellDef class="px-1 text-center" i18n mat-header-cell></th> |
|||
<td *matCellDef="let element" class="px-1 text-center" mat-cell> |
|||
<button |
|||
class="mx-1 no-min-width px-2" |
|||
mat-button |
|||
[matMenuTriggerFor]="tagMenu" |
|||
(click)="$event.stopPropagation()" |
|||
> |
|||
<ion-icon name="ellipsis-horizontal" /> |
|||
</button> |
|||
<mat-menu #tagMenu="matMenu" xPosition="before"> |
|||
<button mat-menu-item (click)="onUpdateTag(element)"> |
|||
<span class="align-items-center d-flex"> |
|||
<ion-icon class="mr-2" name="create-outline" /> |
|||
<span i18n>Edit</span> |
|||
</span> |
|||
</button> |
|||
<hr class="m-0" /> |
|||
<button |
|||
mat-menu-item |
|||
[disabled]="element.activityCount > 0" |
|||
(click)="onDeleteTag(element.id)" |
|||
> |
|||
<span class="align-items-center d-flex"> |
|||
<ion-icon class="mr-2" name="trash-outline" /> |
|||
<span i18n>Delete</span> |
|||
</span> |
|||
</button> |
|||
</mat-menu> |
|||
</td> |
|||
</ng-container> |
|||
|
|||
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr> |
|||
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr> |
|||
</table> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr> |
|||
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr> |
|||
</table> |
|||
|
@ -0,0 +1,3 @@ |
|||
:host { |
|||
display: block; |
|||
} |
@ -0,0 +1,92 @@ |
|||
import { GfSymbolAutocompleteComponent } from '@ghostfolio/ui/symbol-autocomplete'; |
|||
|
|||
import { CommonModule } from '@angular/common'; |
|||
import { |
|||
ChangeDetectionStrategy, |
|||
Component, |
|||
OnDestroy, |
|||
OnInit |
|||
} from '@angular/core'; |
|||
import { |
|||
AbstractControl, |
|||
FormBuilder, |
|||
FormControl, |
|||
FormGroup, |
|||
FormsModule, |
|||
ReactiveFormsModule, |
|||
ValidationErrors, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { MatButtonModule } from '@angular/material/button'; |
|||
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; |
|||
import { MatFormFieldModule } from '@angular/material/form-field'; |
|||
import { Subject } from 'rxjs'; |
|||
|
|||
@Component({ |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
host: { class: 'h-100' }, |
|||
imports: [ |
|||
CommonModule, |
|||
FormsModule, |
|||
GfSymbolAutocompleteComponent, |
|||
MatButtonModule, |
|||
MatDialogModule, |
|||
MatFormFieldModule, |
|||
ReactiveFormsModule |
|||
], |
|||
selector: 'gf-create-watchlist-item-dialog', |
|||
styleUrls: ['./create-watchlist-item-dialog.component.scss'], |
|||
templateUrl: 'create-watchlist-item-dialog.html' |
|||
}) |
|||
export class CreateWatchlistItemDialogComponent implements OnInit, OnDestroy { |
|||
public createWatchlistItemForm: FormGroup; |
|||
|
|||
private unsubscribeSubject = new Subject<void>(); |
|||
|
|||
public constructor( |
|||
public readonly dialogRef: MatDialogRef<CreateWatchlistItemDialogComponent>, |
|||
public readonly formBuilder: FormBuilder |
|||
) {} |
|||
|
|||
public ngOnInit() { |
|||
this.createWatchlistItemForm = this.formBuilder.group( |
|||
{ |
|||
searchSymbol: new FormControl(null, [Validators.required]) |
|||
}, |
|||
{ |
|||
validators: this.validator |
|||
} |
|||
); |
|||
} |
|||
|
|||
public onCancel() { |
|||
this.dialogRef.close(); |
|||
} |
|||
|
|||
public onSubmit() { |
|||
this.dialogRef.close({ |
|||
dataSource: |
|||
this.createWatchlistItemForm.get('searchSymbol').value.dataSource, |
|||
symbol: this.createWatchlistItemForm.get('searchSymbol').value.symbol |
|||
}); |
|||
} |
|||
|
|||
public ngOnDestroy() { |
|||
this.unsubscribeSubject.next(); |
|||
this.unsubscribeSubject.complete(); |
|||
} |
|||
|
|||
private validator(control: AbstractControl): ValidationErrors { |
|||
const searchSymbolControl = control.get('searchSymbol'); |
|||
|
|||
if ( |
|||
searchSymbolControl.valid && |
|||
searchSymbolControl.value.dataSource && |
|||
searchSymbolControl.value.symbol |
|||
) { |
|||
return { incomplete: false }; |
|||
} |
|||
|
|||
return { incomplete: true }; |
|||
} |
|||
} |
@ -0,0 +1,25 @@ |
|||
<form |
|||
class="d-flex flex-column h-100" |
|||
[formGroup]="createWatchlistItemForm" |
|||
(keyup.enter)="createWatchlistItemForm.valid && onSubmit()" |
|||
(ngSubmit)="onSubmit()" |
|||
> |
|||
<h1 i18n mat-dialog-title>Add asset to watchlist</h1> |
|||
<div class="flex-grow-1 py-3" mat-dialog-content> |
|||
<mat-form-field appearance="outline" class="w-100"> |
|||
<mat-label i18n>Name, symbol or ISIN</mat-label> |
|||
<gf-symbol-autocomplete formControlName="searchSymbol" /> |
|||
</mat-form-field> |
|||
</div> |
|||
<div class="d-flex justify-content-end" mat-dialog-actions> |
|||
<button i18n mat-button type="button" (click)="onCancel()">Cancel</button> |
|||
<button |
|||
color="primary" |
|||
mat-flat-button |
|||
type="submit" |
|||
[disabled]="createWatchlistItemForm.hasError('incomplete')" |
|||
> |
|||
<ng-container i18n>Save</ng-container> |
|||
</button> |
|||
</div> |
|||
</form> |
@ -0,0 +1,4 @@ |
|||
export interface CreateWatchlistItemDialogParams { |
|||
deviceType: string; |
|||
locale: string; |
|||
} |
@ -0,0 +1,185 @@ |
|||
import { DataService } from '@ghostfolio/client/services/data.service'; |
|||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; |
|||
import { UserService } from '@ghostfolio/client/services/user/user.service'; |
|||
import { |
|||
AssetProfileIdentifier, |
|||
Benchmark, |
|||
User |
|||
} from '@ghostfolio/common/interfaces'; |
|||
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; |
|||
import { BenchmarkTrend } from '@ghostfolio/common/types'; |
|||
import { GfBenchmarkComponent } from '@ghostfolio/ui/benchmark'; |
|||
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; |
|||
|
|||
import { CommonModule } from '@angular/common'; |
|||
import { |
|||
ChangeDetectionStrategy, |
|||
ChangeDetectorRef, |
|||
Component, |
|||
CUSTOM_ELEMENTS_SCHEMA, |
|||
OnDestroy, |
|||
OnInit |
|||
} from '@angular/core'; |
|||
import { MatButtonModule } from '@angular/material/button'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { ActivatedRoute, Router, RouterModule } from '@angular/router'; |
|||
import { DeviceDetectorService } from 'ngx-device-detector'; |
|||
import { Subject } from 'rxjs'; |
|||
import { takeUntil } from 'rxjs/operators'; |
|||
|
|||
import { CreateWatchlistItemDialogComponent } from './create-watchlist-item-dialog/create-watchlist-item-dialog.component'; |
|||
import { CreateWatchlistItemDialogParams } from './create-watchlist-item-dialog/interfaces/interfaces'; |
|||
|
|||
@Component({ |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
imports: [ |
|||
CommonModule, |
|||
GfBenchmarkComponent, |
|||
GfPremiumIndicatorComponent, |
|||
MatButtonModule, |
|||
RouterModule |
|||
], |
|||
schemas: [CUSTOM_ELEMENTS_SCHEMA], |
|||
selector: 'gf-home-watchlist', |
|||
styleUrls: ['./home-watchlist.scss'], |
|||
templateUrl: './home-watchlist.html' |
|||
}) |
|||
export class HomeWatchlistComponent implements OnDestroy, OnInit { |
|||
public deviceType: string; |
|||
public hasImpersonationId: boolean; |
|||
public hasPermissionToCreateWatchlistItem: boolean; |
|||
public hasPermissionToDeleteWatchlistItem: boolean; |
|||
public user: User; |
|||
public watchlist: Benchmark[]; |
|||
|
|||
private unsubscribeSubject = new Subject<void>(); |
|||
|
|||
public constructor( |
|||
private changeDetectorRef: ChangeDetectorRef, |
|||
private dataService: DataService, |
|||
private deviceService: DeviceDetectorService, |
|||
private dialog: MatDialog, |
|||
private impersonationStorageService: ImpersonationStorageService, |
|||
private route: ActivatedRoute, |
|||
private router: Router, |
|||
private userService: UserService |
|||
) { |
|||
this.deviceType = this.deviceService.getDeviceInfo().deviceType; |
|||
|
|||
this.impersonationStorageService |
|||
.onChangeHasImpersonation() |
|||
.pipe(takeUntil(this.unsubscribeSubject)) |
|||
.subscribe((impersonationId) => { |
|||
this.hasImpersonationId = !!impersonationId; |
|||
}); |
|||
|
|||
this.route.queryParams |
|||
.pipe(takeUntil(this.unsubscribeSubject)) |
|||
.subscribe((params) => { |
|||
if (params['createWatchlistItemDialog']) { |
|||
this.openCreateWatchlistItemDialog(); |
|||
} |
|||
}); |
|||
|
|||
this.userService.stateChanged |
|||
.pipe(takeUntil(this.unsubscribeSubject)) |
|||
.subscribe((state) => { |
|||
if (state?.user) { |
|||
this.user = state.user; |
|||
|
|||
this.hasPermissionToCreateWatchlistItem = |
|||
!this.hasImpersonationId && |
|||
hasPermission( |
|||
this.user.permissions, |
|||
permissions.createWatchlistItem |
|||
); |
|||
this.hasPermissionToDeleteWatchlistItem = |
|||
!this.hasImpersonationId && |
|||
hasPermission( |
|||
this.user.permissions, |
|||
permissions.deleteWatchlistItem |
|||
); |
|||
|
|||
this.changeDetectorRef.markForCheck(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
public ngOnInit() { |
|||
this.loadWatchlistData(); |
|||
} |
|||
|
|||
public onWatchlistItemDeleted({ |
|||
dataSource, |
|||
symbol |
|||
}: AssetProfileIdentifier) { |
|||
this.dataService |
|||
.deleteWatchlistItem({ dataSource, symbol }) |
|||
.pipe(takeUntil(this.unsubscribeSubject)) |
|||
.subscribe({ |
|||
next: () => { |
|||
return this.loadWatchlistData(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
public ngOnDestroy() { |
|||
this.unsubscribeSubject.next(); |
|||
this.unsubscribeSubject.complete(); |
|||
} |
|||
|
|||
private loadWatchlistData() { |
|||
this.dataService |
|||
.fetchWatchlist() |
|||
.pipe(takeUntil(this.unsubscribeSubject)) |
|||
.subscribe(({ watchlist }) => { |
|||
this.watchlist = watchlist.map( |
|||
({ dataSource, marketCondition, name, performances, symbol }) => ({ |
|||
dataSource, |
|||
marketCondition, |
|||
name, |
|||
performances, |
|||
symbol, |
|||
trend50d: 'UNKNOWN' as BenchmarkTrend, |
|||
trend200d: 'UNKNOWN' as BenchmarkTrend |
|||
}) |
|||
); |
|||
|
|||
this.changeDetectorRef.markForCheck(); |
|||
}); |
|||
} |
|||
|
|||
private openCreateWatchlistItemDialog() { |
|||
this.userService |
|||
.get() |
|||
.pipe(takeUntil(this.unsubscribeSubject)) |
|||
.subscribe((user) => { |
|||
this.user = user; |
|||
|
|||
const dialogRef = this.dialog.open(CreateWatchlistItemDialogComponent, { |
|||
autoFocus: false, |
|||
data: { |
|||
deviceType: this.deviceType, |
|||
locale: this.user?.settings?.locale |
|||
} as CreateWatchlistItemDialogParams, |
|||
width: this.deviceType === 'mobile' ? '100vw' : '50rem' |
|||
}); |
|||
|
|||
dialogRef |
|||
.afterClosed() |
|||
.pipe(takeUntil(this.unsubscribeSubject)) |
|||
.subscribe(({ dataSource, symbol } = {}) => { |
|||
if (dataSource && symbol) { |
|||
this.dataService |
|||
.postWatchlistItem({ dataSource, symbol }) |
|||
.pipe(takeUntil(this.unsubscribeSubject)) |
|||
.subscribe({ |
|||
next: () => this.loadWatchlistData() |
|||
}); |
|||
} |
|||
|
|||
this.router.navigate(['.'], { relativeTo: this.route }); |
|||
}); |
|||
}); |
|||
} |
|||
} |
@ -0,0 +1,35 @@ |
|||
<div class="container"> |
|||
<h1 class="d-none d-sm-block h3 mb-4"> |
|||
<span class="align-items-center d-flex justify-content-center"> |
|||
<span i18n>Watchlist</span> |
|||
@if (user?.subscription?.type === 'Basic') { |
|||
<gf-premium-indicator class="ml-1" /> |
|||
} |
|||
</span> |
|||
</h1> |
|||
<div class="row"> |
|||
<div class="col-xs-12 col-md-8 offset-md-2"> |
|||
<gf-benchmark |
|||
[benchmarks]="watchlist" |
|||
[deviceType]="deviceType" |
|||
[hasPermissionToDeleteItem]="hasPermissionToDeleteWatchlistItem" |
|||
[locale]="user?.settings?.locale || undefined" |
|||
[user]="user" |
|||
(itemDeleted)="onWatchlistItemDeleted($event)" |
|||
/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
@if (!hasImpersonationId && hasPermissionToCreateWatchlistItem) { |
|||
<div class="fab-container"> |
|||
<a |
|||
class="align-items-center d-flex justify-content-center" |
|||
color="primary" |
|||
mat-fab |
|||
[queryParams]="{ createWatchlistItemDialog: true }" |
|||
[routerLink]="[]" |
|||
> |
|||
<ion-icon name="add-outline" size="large" /> |
|||
</a> |
|||
</div> |
|||
} |
@ -0,0 +1,3 @@ |
|||
:host { |
|||
display: block; |
|||
} |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue