Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
07656c6a95 | |||
16f0743353 | |||
9b5ec0c56d | |||
8d2fcc6b42 | |||
e625e55784 | |||
bed3e5aae2 | |||
65bfe52db4 | |||
48b524de5a | |||
67d40333f6 | |||
48f6b8d353 | |||
f369996912 | |||
dc424a86ec | |||
5d8bde5a70 | |||
16360c0c67 | |||
526a6b2030 | |||
5000e9c79b | |||
161cb82820 |
48
CHANGELOG.md
48
CHANGELOG.md
@ -5,6 +5,54 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## 1.112.0 - 06.02.2022
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the export functionality to the position detail dialog
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the export functionality for activities (respect filtering)
|
||||||
|
- Removed the _Admin_ user from the database seeding
|
||||||
|
- Assigned the role `ADMIN` on sign up (only if there is no admin yet)
|
||||||
|
- Upgraded `prisma` from version `3.8.1` to `3.9.1`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue with the performance calculation in connection with a sell activity in the new calculation engine
|
||||||
|
- Fixed the horizontal overflow in the accounts table
|
||||||
|
- Fixed the horizontal overflow in the activities table
|
||||||
|
- Fixed the total value of the activities table in the position detail dialog (absolute value)
|
||||||
|
|
||||||
|
### Todo
|
||||||
|
|
||||||
|
- Apply data migration (`yarn database:migrate`)
|
||||||
|
|
||||||
|
## 1.111.0 - 03.02.2022
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support for deleting symbol profile data in the admin control panel
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Used `dataSource` and `symbol` from `SymbolProfile` instead of the `order` object (in `ExportService` and `PortfolioService`)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the symbol selection of the 7d data gathering
|
||||||
|
|
||||||
|
## 1.110.0 - 02.02.2022
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the data source of the _Fear & Greed Index_ (market mood)
|
||||||
|
|
||||||
|
### Todo
|
||||||
|
|
||||||
|
- Apply data migration (`yarn database:migrate`)
|
||||||
|
|
||||||
## 1.109.0 - 01.02.2022
|
## 1.109.0 - 01.02.2022
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
12
README.md
12
README.md
@ -124,16 +124,10 @@ docker-compose -f docker/docker-compose.build.yml exec ghostfolio yarn database:
|
|||||||
|
|
||||||
Open http://localhost:3333 in your browser and accomplish these steps:
|
Open http://localhost:3333 in your browser and accomplish these steps:
|
||||||
|
|
||||||
1. Login as _Admin_ with the following _Security Token_: `ae76872ae8f3419c6d6f64bf51888ecbcc703927a342d815fafe486acdb938da07d0cf44fca211a0be74a423238f535362d390a41e81e633a9ce668a6e31cdf9`
|
1. Create a new user via _Get Started_ (this first user will get the role `ADMIN`)
|
||||||
1. Go to the _Admin Control Panel_ and click _Gather All Data_ to fetch historical data
|
1. Go to the _Admin Control Panel_ and click _Gather All Data_ to fetch historical data
|
||||||
1. Click _Sign out_ and check out the _Live Demo_
|
1. Click _Sign out_ and check out the _Live Demo_
|
||||||
|
|
||||||
### Finalization
|
|
||||||
|
|
||||||
1. Create a new user via _Get Started_
|
|
||||||
1. Assign the role `ADMIN` to this user (directly in the database)
|
|
||||||
1. Delete the original _Admin_ (directly in the database)
|
|
||||||
|
|
||||||
### Migrate Database
|
### Migrate Database
|
||||||
|
|
||||||
With the following command you can keep your database schema in sync after a Ghostfolio version update:
|
With the following command you can keep your database schema in sync after a Ghostfolio version update:
|
||||||
@ -155,8 +149,8 @@ docker-compose -f docker/docker-compose-build-local.yml exec ghostfolio yarn dat
|
|||||||
1. Run `yarn install`
|
1. Run `yarn install`
|
||||||
1. Run `docker-compose -f docker/docker-compose.dev.yml up -d` to start [PostgreSQL](https://www.postgresql.org) and [Redis](https://redis.io)
|
1. Run `docker-compose -f docker/docker-compose.dev.yml up -d` to start [PostgreSQL](https://www.postgresql.org) and [Redis](https://redis.io)
|
||||||
1. Run `yarn database:setup` to initialize the database schema and populate your database with (example) data
|
1. Run `yarn database:setup` to initialize the database schema and populate your database with (example) data
|
||||||
1. Start server and client (see [_Development_](#Development))
|
1. Start the server and the client (see [_Development_](#Development))
|
||||||
1. Login as _Admin_ with the following _Security Token_: `ae76872ae8f3419c6d6f64bf51888ecbcc703927a342d815fafe486acdb938da07d0cf44fca211a0be74a423238f535362d390a41e81e633a9ce668a6e31cdf9`
|
1. Create a new user via _Get Started_ (this first user will get the role `ADMIN`)
|
||||||
1. Go to the _Admin Control Panel_ and click _Gather All Data_ to fetch historical data
|
1. Go to the _Admin Control Panel_ and click _Gather All Data_ to fetch historical data
|
||||||
1. Click _Sign out_ and check out the _Live Demo_
|
1. Click _Sign out_ and check out the _Live Demo_
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ import type { RequestWithUser } from '@ghostfolio/common/types';
|
|||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
|
Delete,
|
||||||
Get,
|
Get,
|
||||||
HttpException,
|
HttpException,
|
||||||
Inject,
|
Inject,
|
||||||
@ -195,9 +196,10 @@ export class AdminController {
|
|||||||
return this.adminService.getMarketData();
|
return this.adminService.getMarketData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('market-data/:symbol')
|
@Get('market-data/:dataSource/:symbol')
|
||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
public async getMarketDataBySymbol(
|
public async getMarketDataBySymbol(
|
||||||
|
@Param('dataSource') dataSource: DataSource,
|
||||||
@Param('symbol') symbol: string
|
@Param('symbol') symbol: string
|
||||||
): Promise<AdminMarketDataDetails> {
|
): Promise<AdminMarketDataDetails> {
|
||||||
if (
|
if (
|
||||||
@ -212,7 +214,7 @@ export class AdminController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.adminService.getMarketDataBySymbol(symbol);
|
return this.adminService.getMarketDataBySymbol({ dataSource, symbol });
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put('market-data/:dataSource/:symbol/:dateString')
|
@Put('market-data/:dataSource/:symbol/:dateString')
|
||||||
@ -248,6 +250,27 @@ export class AdminController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Delete('profile-data/:dataSource/:symbol')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
public async deleteProfileData(
|
||||||
|
@Param('dataSource') dataSource: DataSource,
|
||||||
|
@Param('symbol') symbol: string
|
||||||
|
): Promise<void> {
|
||||||
|
if (
|
||||||
|
!hasPermission(
|
||||||
|
this.request.user.permissions,
|
||||||
|
permissions.accessAdminControl
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
|
StatusCodes.FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.adminService.deleteProfileData({ dataSource, symbol });
|
||||||
|
}
|
||||||
|
|
||||||
@Put('settings/:key')
|
@Put('settings/:key')
|
||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
public async updateProperty(
|
public async updateProperty(
|
||||||
|
@ -6,6 +6,7 @@ import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-d
|
|||||||
import { MarketDataModule } from '@ghostfolio/api/services/market-data.module';
|
import { MarketDataModule } from '@ghostfolio/api/services/market-data.module';
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
||||||
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
|
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
|
||||||
|
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile.module';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { AdminController } from './admin.controller';
|
import { AdminController } from './admin.controller';
|
||||||
@ -20,7 +21,8 @@ import { AdminService } from './admin.service';
|
|||||||
MarketDataModule,
|
MarketDataModule,
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
PropertyModule,
|
PropertyModule,
|
||||||
SubscriptionModule
|
SubscriptionModule,
|
||||||
|
SymbolProfileModule
|
||||||
],
|
],
|
||||||
controllers: [AdminController],
|
controllers: [AdminController],
|
||||||
providers: [AdminService],
|
providers: [AdminService],
|
||||||
|
@ -5,6 +5,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
|
|||||||
import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
|
import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||||
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
||||||
import { PROPERTY_CURRENCIES, baseCurrency } from '@ghostfolio/common/config';
|
import { PROPERTY_CURRENCIES, baseCurrency } from '@ghostfolio/common/config';
|
||||||
import {
|
import {
|
||||||
AdminData,
|
AdminData,
|
||||||
@ -13,7 +14,7 @@ import {
|
|||||||
AdminMarketDataItem
|
AdminMarketDataItem
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Property } from '@prisma/client';
|
import { DataSource, Property } from '@prisma/client';
|
||||||
import { differenceInDays } from 'date-fns';
|
import { differenceInDays } from 'date-fns';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -25,9 +26,21 @@ export class AdminService {
|
|||||||
private readonly marketDataService: MarketDataService,
|
private readonly marketDataService: MarketDataService,
|
||||||
private readonly prismaService: PrismaService,
|
private readonly prismaService: PrismaService,
|
||||||
private readonly propertyService: PropertyService,
|
private readonly propertyService: PropertyService,
|
||||||
private readonly subscriptionService: SubscriptionService
|
private readonly subscriptionService: SubscriptionService,
|
||||||
|
private readonly symbolProfileService: SymbolProfileService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
public async deleteProfileData({
|
||||||
|
dataSource,
|
||||||
|
symbol
|
||||||
|
}: {
|
||||||
|
dataSource: DataSource;
|
||||||
|
symbol: string;
|
||||||
|
}) {
|
||||||
|
await this.marketDataService.deleteMany({ dataSource, symbol });
|
||||||
|
await this.symbolProfileService.delete({ dataSource, symbol });
|
||||||
|
}
|
||||||
|
|
||||||
public async get(): Promise<AdminData> {
|
public async get(): Promise<AdminData> {
|
||||||
return {
|
return {
|
||||||
dataGatheringProgress:
|
dataGatheringProgress:
|
||||||
@ -121,16 +134,21 @@ export class AdminService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getMarketDataBySymbol(
|
public async getMarketDataBySymbol({
|
||||||
aSymbol: string
|
dataSource,
|
||||||
): Promise<AdminMarketDataDetails> {
|
symbol
|
||||||
|
}: {
|
||||||
|
dataSource: DataSource;
|
||||||
|
symbol: string;
|
||||||
|
}): Promise<AdminMarketDataDetails> {
|
||||||
return {
|
return {
|
||||||
marketData: await this.marketDataService.marketDataItems({
|
marketData: await this.marketDataService.marketDataItems({
|
||||||
orderBy: {
|
orderBy: {
|
||||||
date: 'asc'
|
date: 'asc'
|
||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
symbol: aSymbol
|
dataSource,
|
||||||
|
symbol
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
import { Export } from '@ghostfolio/common/interfaces';
|
import { Export } from '@ghostfolio/common/interfaces';
|
||||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import { Controller, Get, Inject, UseGuards } from '@nestjs/common';
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Headers,
|
||||||
|
Inject,
|
||||||
|
Query,
|
||||||
|
UseGuards
|
||||||
|
} from '@nestjs/common';
|
||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
|
||||||
@ -15,8 +22,11 @@ export class ExportController {
|
|||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
public async export(): Promise<Export> {
|
public async export(
|
||||||
return await this.exportService.export({
|
@Query('activityIds') activityIds?: string[]
|
||||||
|
): Promise<Export> {
|
||||||
|
return this.exportService.export({
|
||||||
|
activityIds,
|
||||||
userId: this.request.user.id
|
userId: this.request.user.id
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -7,8 +7,14 @@ import { Injectable } from '@nestjs/common';
|
|||||||
export class ExportService {
|
export class ExportService {
|
||||||
public constructor(private readonly prismaService: PrismaService) {}
|
public constructor(private readonly prismaService: PrismaService) {}
|
||||||
|
|
||||||
public async export({ userId }: { userId: string }): Promise<Export> {
|
public async export({
|
||||||
const orders = await this.prismaService.order.findMany({
|
activityIds,
|
||||||
|
userId
|
||||||
|
}: {
|
||||||
|
activityIds?: string[];
|
||||||
|
userId: string;
|
||||||
|
}): Promise<Export> {
|
||||||
|
let orders = await this.prismaService.order.findMany({
|
||||||
orderBy: { date: 'desc' },
|
orderBy: { date: 'desc' },
|
||||||
select: {
|
select: {
|
||||||
accountId: true,
|
accountId: true,
|
||||||
@ -16,17 +22,47 @@ export class ExportService {
|
|||||||
dataSource: true,
|
dataSource: true,
|
||||||
date: true,
|
date: true,
|
||||||
fee: true,
|
fee: true,
|
||||||
|
id: true,
|
||||||
quantity: true,
|
quantity: true,
|
||||||
symbol: true,
|
SymbolProfile: true,
|
||||||
type: true,
|
type: true,
|
||||||
unitPrice: true
|
unitPrice: true
|
||||||
},
|
},
|
||||||
where: { userId }
|
where: { userId }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (activityIds) {
|
||||||
|
orders = orders.filter((order) => {
|
||||||
|
return activityIds.includes(order.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
meta: { date: new Date().toISOString(), version: environment.version },
|
meta: { date: new Date().toISOString(), version: environment.version },
|
||||||
orders
|
orders: orders.map(
|
||||||
|
({
|
||||||
|
accountId,
|
||||||
|
currency,
|
||||||
|
date,
|
||||||
|
fee,
|
||||||
|
quantity,
|
||||||
|
SymbolProfile,
|
||||||
|
type,
|
||||||
|
unitPrice
|
||||||
|
}) => {
|
||||||
|
return {
|
||||||
|
accountId,
|
||||||
|
currency,
|
||||||
|
date,
|
||||||
|
fee,
|
||||||
|
quantity,
|
||||||
|
type,
|
||||||
|
unitPrice,
|
||||||
|
dataSource: SymbolProfile.dataSource,
|
||||||
|
symbol: SymbolProfile.symbol
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,12 +11,14 @@ import {
|
|||||||
PROPERTY_STRIPE_CONFIG,
|
PROPERTY_STRIPE_CONFIG,
|
||||||
PROPERTY_SYSTEM_MESSAGE
|
PROPERTY_SYSTEM_MESSAGE
|
||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
|
import { encodeDataSource } from '@ghostfolio/common/helper';
|
||||||
import { InfoItem } from '@ghostfolio/common/interfaces';
|
import { InfoItem } from '@ghostfolio/common/interfaces';
|
||||||
import { Statistics } from '@ghostfolio/common/interfaces/statistics.interface';
|
import { Statistics } from '@ghostfolio/common/interfaces/statistics.interface';
|
||||||
import { Subscription } from '@ghostfolio/common/interfaces/subscription.interface';
|
import { Subscription } from '@ghostfolio/common/interfaces/subscription.interface';
|
||||||
import { permissions } from '@ghostfolio/common/permissions';
|
import { permissions } from '@ghostfolio/common/permissions';
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
import { DataSource } from '@prisma/client';
|
||||||
import * as bent from 'bent';
|
import * as bent from 'bent';
|
||||||
import { subDays } from 'date-fns';
|
import { subDays } from 'date-fns';
|
||||||
|
|
||||||
@ -49,6 +51,10 @@ export class InfoService {
|
|||||||
globalPermissions.push(permissions.enableBlog);
|
globalPermissions.push(permissions.enableBlog);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) {
|
||||||
|
info.fearAndGreedDataSource = encodeDataSource(DataSource.RAKUTEN);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.configurationService.get('ENABLE_FEATURE_IMPORT')) {
|
if (this.configurationService.get('ENABLE_FEATURE_IMPORT')) {
|
||||||
globalPermissions.push(permissions.enableImport);
|
globalPermissions.push(permissions.enableImport);
|
||||||
}
|
}
|
||||||
|
@ -337,8 +337,8 @@ export class PortfolioCalculatorNew {
|
|||||||
let grossPerformanceFromSells = new Big(0);
|
let grossPerformanceFromSells = new Big(0);
|
||||||
let initialValue: Big;
|
let initialValue: Big;
|
||||||
let lastAveragePrice = new Big(0);
|
let lastAveragePrice = new Big(0);
|
||||||
let lastValueOfInvestment = new Big(0);
|
let lastTransactionInvestment = new Big(0);
|
||||||
let lastNetValueOfInvestment = new Big(0);
|
let lastValueOfInvestmentBeforeTransaction = new Big(0);
|
||||||
let timeWeightedGrossPerformancePercentage = new Big(1);
|
let timeWeightedGrossPerformancePercentage = new Big(1);
|
||||||
let timeWeightedNetPerformancePercentage = new Big(1);
|
let timeWeightedNetPerformancePercentage = new Big(1);
|
||||||
let totalInvestment = new Big(0);
|
let totalInvestment = new Big(0);
|
||||||
@ -394,7 +394,13 @@ export class PortfolioCalculatorNew {
|
|||||||
for (let i = 0; i < orders.length; i += 1) {
|
for (let i = 0; i < orders.length; i += 1) {
|
||||||
const order = orders[i];
|
const order = orders[i];
|
||||||
|
|
||||||
const transactionInvestment = order.quantity.mul(order.unitPrice);
|
const valueOfInvestmentBeforeTransaction = totalUnits.mul(
|
||||||
|
order.unitPrice
|
||||||
|
);
|
||||||
|
|
||||||
|
const transactionInvestment = order.quantity
|
||||||
|
.mul(order.unitPrice)
|
||||||
|
.mul(this.getFactor(order.type));
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!initialValue &&
|
!initialValue &&
|
||||||
@ -411,7 +417,6 @@ export class PortfolioCalculatorNew {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const valueOfInvestment = totalUnits.mul(order.unitPrice);
|
const valueOfInvestment = totalUnits.mul(order.unitPrice);
|
||||||
const netValueOfInvestment = totalUnits.mul(order.unitPrice).sub(fees);
|
|
||||||
|
|
||||||
const grossPerformanceFromSell =
|
const grossPerformanceFromSell =
|
||||||
order.type === TypeOfOrder.SELL
|
order.type === TypeOfOrder.SELL
|
||||||
@ -423,7 +428,7 @@ export class PortfolioCalculatorNew {
|
|||||||
);
|
);
|
||||||
|
|
||||||
totalInvestment = totalInvestment
|
totalInvestment = totalInvestment
|
||||||
.plus(transactionInvestment.mul(this.getFactor(order.type)))
|
.plus(transactionInvestment)
|
||||||
.plus(grossPerformanceFromSell);
|
.plus(grossPerformanceFromSell);
|
||||||
|
|
||||||
lastAveragePrice = totalUnits.eq(0)
|
lastAveragePrice = totalUnits.eq(0)
|
||||||
@ -436,48 +441,52 @@ export class PortfolioCalculatorNew {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
i > indexOfStartOrder &&
|
i > indexOfStartOrder &&
|
||||||
!lastValueOfInvestment
|
!lastValueOfInvestmentBeforeTransaction
|
||||||
.plus(transactionInvestment.mul(this.getFactor(order.type)))
|
.plus(lastTransactionInvestment)
|
||||||
.eq(0)
|
.eq(0)
|
||||||
) {
|
) {
|
||||||
timeWeightedGrossPerformancePercentage =
|
const grossHoldingPeriodReturn = valueOfInvestmentBeforeTransaction
|
||||||
timeWeightedGrossPerformancePercentage.mul(
|
.sub(
|
||||||
new Big(1).plus(
|
lastValueOfInvestmentBeforeTransaction.plus(
|
||||||
valueOfInvestment
|
lastTransactionInvestment
|
||||||
.minus(
|
|
||||||
lastValueOfInvestment.plus(
|
|
||||||
transactionInvestment.mul(this.getFactor(order.type))
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.div(
|
.div(
|
||||||
lastValueOfInvestment.plus(
|
lastValueOfInvestmentBeforeTransaction.plus(
|
||||||
transactionInvestment.mul(this.getFactor(order.type))
|
lastTransactionInvestment
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
timeWeightedGrossPerformancePercentage =
|
||||||
|
timeWeightedGrossPerformancePercentage.mul(
|
||||||
|
new Big(1).plus(grossHoldingPeriodReturn)
|
||||||
|
);
|
||||||
|
|
||||||
|
const netHoldingPeriodReturn = valueOfInvestmentBeforeTransaction
|
||||||
|
.sub(fees.sub(order.fee))
|
||||||
|
.sub(
|
||||||
|
lastValueOfInvestmentBeforeTransaction.plus(
|
||||||
|
lastTransactionInvestment
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.div(
|
||||||
|
lastValueOfInvestmentBeforeTransaction.plus(
|
||||||
|
lastTransactionInvestment
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
timeWeightedNetPerformancePercentage =
|
timeWeightedNetPerformancePercentage =
|
||||||
timeWeightedNetPerformancePercentage.mul(
|
timeWeightedNetPerformancePercentage.mul(
|
||||||
new Big(1).plus(
|
new Big(1).plus(netHoldingPeriodReturn)
|
||||||
netValueOfInvestment
|
|
||||||
.minus(
|
|
||||||
lastNetValueOfInvestment.plus(
|
|
||||||
transactionInvestment.mul(this.getFactor(order.type))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.div(
|
|
||||||
lastNetValueOfInvestment.plus(
|
|
||||||
transactionInvestment.mul(this.getFactor(order.type))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
grossPerformance = newGrossPerformance;
|
grossPerformance = newGrossPerformance;
|
||||||
lastNetValueOfInvestment = netValueOfInvestment;
|
|
||||||
lastValueOfInvestment = valueOfInvestment;
|
lastTransactionInvestment = transactionInvestment;
|
||||||
|
|
||||||
|
lastValueOfInvestmentBeforeTransaction =
|
||||||
|
valueOfInvestmentBeforeTransaction;
|
||||||
|
|
||||||
if (order.itemType === 'start') {
|
if (order.itemType === 'start') {
|
||||||
feesAtStartDate = fees;
|
feesAtStartDate = fees;
|
||||||
|
@ -407,8 +407,11 @@ export class PortfolioServiceNew {
|
|||||||
|
|
||||||
const orders = (
|
const orders = (
|
||||||
await this.orderService.getOrders({ userCurrency, userId })
|
await this.orderService.getOrders({ userCurrency, userId })
|
||||||
).filter((order) => {
|
).filter(({ SymbolProfile }) => {
|
||||||
return order.dataSource === aDataSource && order.symbol === aSymbol;
|
return (
|
||||||
|
SymbolProfile.dataSource === aDataSource &&
|
||||||
|
SymbolProfile.symbol === aSymbol
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (orders.length <= 0) {
|
if (orders.length <= 0) {
|
||||||
|
@ -395,8 +395,11 @@ export class PortfolioService {
|
|||||||
|
|
||||||
const orders = (
|
const orders = (
|
||||||
await this.orderService.getOrders({ userCurrency, userId })
|
await this.orderService.getOrders({ userCurrency, userId })
|
||||||
).filter((order) => {
|
).filter(({ SymbolProfile }) => {
|
||||||
return order.dataSource === aDataSource && order.symbol === aSymbol;
|
return (
|
||||||
|
SymbolProfile.dataSource === aDataSource &&
|
||||||
|
SymbolProfile.symbol === aSymbol
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (orders.length <= 0) {
|
if (orders.length <= 0) {
|
||||||
|
@ -23,7 +23,7 @@ import {
|
|||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { Provider } from '@prisma/client';
|
import { Provider, Role } from '@prisma/client';
|
||||||
import { User as UserModel } from '@prisma/client';
|
import { User as UserModel } from '@prisma/client';
|
||||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
|
||||||
@ -83,8 +83,10 @@ export class UserController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasAdmin = await this.userService.hasAdmin();
|
||||||
|
|
||||||
const { accessToken, id } = await this.userService.createUser({
|
const { accessToken, id } = await this.userService.createUser({
|
||||||
provider: Provider.ANONYMOUS
|
role: hasAdmin ? 'USER' : 'ADMIN'
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -70,6 +70,18 @@ export class UserService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async hasAdmin() {
|
||||||
|
const usersWithAdminRole = await this.users({
|
||||||
|
where: {
|
||||||
|
role: {
|
||||||
|
equals: 'ADMIN'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return usersWithAdminRole.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
public isRestrictedView(aUser: UserWithSettings) {
|
public isRestrictedView(aUser: UserWithSettings) {
|
||||||
return (aUser.Settings.settings as UserSettings)?.isRestrictedView ?? false;
|
return (aUser.Settings.settings as UserSettings)?.isRestrictedView ?? false;
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { decodeDataSource } from '@ghostfolio/common/helper';
|
||||||
import {
|
import {
|
||||||
CallHandler,
|
CallHandler,
|
||||||
ExecutionContext,
|
ExecutionContext,
|
||||||
@ -5,6 +6,7 @@ import {
|
|||||||
NestInterceptor
|
NestInterceptor
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
import { ConfigurationService } from '../services/configuration.service';
|
import { ConfigurationService } from '../services/configuration.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -24,22 +26,14 @@ export class TransformDataSourceInRequestInterceptor<T>
|
|||||||
|
|
||||||
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') === true) {
|
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') === true) {
|
||||||
if (request.body.dataSource) {
|
if (request.body.dataSource) {
|
||||||
request.body.dataSource = this.decodeDataSource(
|
request.body.dataSource = decodeDataSource(request.body.dataSource);
|
||||||
request.body.dataSource
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.params.dataSource) {
|
if (request.params.dataSource) {
|
||||||
request.params.dataSource = this.decodeDataSource(
|
request.params.dataSource = decodeDataSource(request.params.dataSource);
|
||||||
request.params.dataSource
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return next.handle();
|
return next.handle();
|
||||||
}
|
}
|
||||||
|
|
||||||
private decodeDataSource(encodeDataSource: string) {
|
|
||||||
return Buffer.from(encodeDataSource, 'hex').toString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
|
import { encodeDataSource } from '@ghostfolio/common/helper';
|
||||||
import {
|
import {
|
||||||
CallHandler,
|
CallHandler,
|
||||||
ExecutionContext,
|
ExecutionContext,
|
||||||
Injectable,
|
Injectable,
|
||||||
NestInterceptor
|
NestInterceptor
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { DataSource } from '@prisma/client';
|
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
import { ConfigurationService } from '../services/configuration.service';
|
import { ConfigurationService } from '../services/configuration.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -28,22 +29,22 @@ export class TransformDataSourceInResponseInterceptor<T>
|
|||||||
) {
|
) {
|
||||||
if (data.activities) {
|
if (data.activities) {
|
||||||
data.activities.map((activity) => {
|
data.activities.map((activity) => {
|
||||||
activity.SymbolProfile.dataSource = this.encodeDataSource(
|
activity.SymbolProfile.dataSource = encodeDataSource(
|
||||||
activity.SymbolProfile.dataSource
|
activity.SymbolProfile.dataSource
|
||||||
);
|
);
|
||||||
activity.dataSource = this.encodeDataSource(activity.dataSource);
|
activity.dataSource = encodeDataSource(activity.dataSource);
|
||||||
return activity;
|
return activity;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.dataSource) {
|
if (data.dataSource) {
|
||||||
data.dataSource = this.encodeDataSource(data.dataSource);
|
data.dataSource = encodeDataSource(data.dataSource);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.holdings) {
|
if (data.holdings) {
|
||||||
for (const symbol of Object.keys(data.holdings)) {
|
for (const symbol of Object.keys(data.holdings)) {
|
||||||
if (data.holdings[symbol].dataSource) {
|
if (data.holdings[symbol].dataSource) {
|
||||||
data.holdings[symbol].dataSource = this.encodeDataSource(
|
data.holdings[symbol].dataSource = encodeDataSource(
|
||||||
data.holdings[symbol].dataSource
|
data.holdings[symbol].dataSource
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -52,14 +53,14 @@ export class TransformDataSourceInResponseInterceptor<T>
|
|||||||
|
|
||||||
if (data.items) {
|
if (data.items) {
|
||||||
data.items.map((item) => {
|
data.items.map((item) => {
|
||||||
item.dataSource = this.encodeDataSource(item.dataSource);
|
item.dataSource = encodeDataSource(item.dataSource);
|
||||||
return item;
|
return item;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.positions) {
|
if (data.positions) {
|
||||||
data.positions.map((position) => {
|
data.positions.map((position) => {
|
||||||
position.dataSource = this.encodeDataSource(position.dataSource);
|
position.dataSource = encodeDataSource(position.dataSource);
|
||||||
return position;
|
return position;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -69,8 +70,4 @@ export class TransformDataSourceInResponseInterceptor<T>
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private encodeDataSource(aDataSource: DataSource) {
|
|
||||||
return Buffer.from(aDataSource, 'utf-8').toString('hex');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -473,9 +473,18 @@ export class DataGatheringService {
|
|||||||
private async getSymbols7D(): Promise<IDataGatheringItem[]> {
|
private async getSymbols7D(): Promise<IDataGatheringItem[]> {
|
||||||
const startDate = subDays(resetHours(new Date()), 7);
|
const startDate = subDays(resetHours(new Date()), 7);
|
||||||
|
|
||||||
|
const symbolProfiles = await this.prismaService.symbolProfile.findMany({
|
||||||
|
orderBy: [{ symbol: 'asc' }],
|
||||||
|
select: {
|
||||||
|
dataSource: true,
|
||||||
|
scraperConfiguration: true,
|
||||||
|
symbol: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Only consider symbols with incomplete market data for the last
|
// Only consider symbols with incomplete market data for the last
|
||||||
// 7 days
|
// 7 days
|
||||||
const symbolsToGather = (
|
const symbolsNotToGather = (
|
||||||
await this.prismaService.marketData.groupBy({
|
await this.prismaService.marketData.groupBy({
|
||||||
_count: true,
|
_count: true,
|
||||||
by: ['symbol'],
|
by: ['symbol'],
|
||||||
@ -485,24 +494,15 @@ export class DataGatheringService {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
.filter((group) => {
|
.filter((group) => {
|
||||||
return group._count < 6;
|
return group._count >= 6;
|
||||||
})
|
})
|
||||||
.map((group) => {
|
.map((group) => {
|
||||||
return group.symbol;
|
return group.symbol;
|
||||||
});
|
});
|
||||||
|
|
||||||
const symbolProfilesToGather = (
|
const symbolProfilesToGather = symbolProfiles
|
||||||
await this.prismaService.symbolProfile.findMany({
|
|
||||||
orderBy: [{ symbol: 'asc' }],
|
|
||||||
select: {
|
|
||||||
dataSource: true,
|
|
||||||
scraperConfiguration: true,
|
|
||||||
symbol: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.filter(({ symbol }) => {
|
.filter(({ symbol }) => {
|
||||||
return symbolsToGather.includes(symbol);
|
return !symbolsNotToGather.includes(symbol);
|
||||||
})
|
})
|
||||||
.map((symbolProfile) => {
|
.map((symbolProfile) => {
|
||||||
return {
|
return {
|
||||||
@ -514,7 +514,7 @@ export class DataGatheringService {
|
|||||||
const currencyPairsToGather = this.exchangeRateDataService
|
const currencyPairsToGather = this.exchangeRateDataService
|
||||||
.getCurrencyPairs()
|
.getCurrencyPairs()
|
||||||
.filter(({ symbol }) => {
|
.filter(({ symbol }) => {
|
||||||
return symbolsToGather.includes(symbol);
|
return !symbolsNotToGather.includes(symbol);
|
||||||
})
|
})
|
||||||
.map(({ dataSource, symbol }) => {
|
.map(({ dataSource, symbol }) => {
|
||||||
return {
|
return {
|
||||||
|
@ -9,6 +9,21 @@ import { DataSource, MarketData, Prisma } from '@prisma/client';
|
|||||||
export class MarketDataService {
|
export class MarketDataService {
|
||||||
public constructor(private readonly prismaService: PrismaService) {}
|
public constructor(private readonly prismaService: PrismaService) {}
|
||||||
|
|
||||||
|
public async deleteMany({
|
||||||
|
dataSource,
|
||||||
|
symbol
|
||||||
|
}: {
|
||||||
|
dataSource: DataSource;
|
||||||
|
symbol: string;
|
||||||
|
}) {
|
||||||
|
return this.prismaService.marketData.deleteMany({
|
||||||
|
where: {
|
||||||
|
dataSource,
|
||||||
|
symbol
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public async get({
|
public async get({
|
||||||
date,
|
date,
|
||||||
symbol
|
symbol
|
||||||
|
@ -4,14 +4,26 @@ import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
|||||||
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
||||||
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Prisma, SymbolProfile } from '@prisma/client';
|
import { DataSource, Prisma, SymbolProfile } from '@prisma/client';
|
||||||
import { continents, countries } from 'countries-list';
|
import { continents, countries } from 'countries-list';
|
||||||
|
|
||||||
import { ScraperConfiguration } from './data-provider/ghostfolio-scraper-api/interfaces/scraper-configuration.interface';
|
import { ScraperConfiguration } from './data-provider/ghostfolio-scraper-api/interfaces/scraper-configuration.interface';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SymbolProfileService {
|
export class SymbolProfileService {
|
||||||
constructor(private readonly prismaService: PrismaService) {}
|
public constructor(private readonly prismaService: PrismaService) {}
|
||||||
|
|
||||||
|
public async delete({
|
||||||
|
dataSource,
|
||||||
|
symbol
|
||||||
|
}: {
|
||||||
|
dataSource: DataSource;
|
||||||
|
symbol: string;
|
||||||
|
}) {
|
||||||
|
return this.prismaService.symbolProfile.delete({
|
||||||
|
where: { dataSource_symbol: { dataSource, symbol } }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public async getSymbolProfiles(
|
public async getSymbolProfiles(
|
||||||
symbols: string[]
|
symbols: string[]
|
||||||
|
@ -46,9 +46,9 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
public ngOnChanges() {
|
public ngOnChanges() {
|
||||||
this.displayedColumns = [
|
this.displayedColumns = [
|
||||||
'account',
|
'account',
|
||||||
'currency',
|
|
||||||
'platform',
|
'platform',
|
||||||
'transactions',
|
'transactions',
|
||||||
|
'currency',
|
||||||
'balance',
|
'balance',
|
||||||
'value'
|
'value'
|
||||||
];
|
];
|
||||||
|
@ -20,6 +20,7 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
templateUrl: './admin-market-data.html'
|
templateUrl: './admin-market-data.html'
|
||||||
})
|
})
|
||||||
export class AdminMarketDataComponent implements OnDestroy, OnInit {
|
export class AdminMarketDataComponent implements OnDestroy, OnInit {
|
||||||
|
public currentDataSource: DataSource;
|
||||||
public currentSymbol: string;
|
public currentSymbol: string;
|
||||||
public defaultDateFormat = DEFAULT_DATE_FORMAT;
|
public defaultDateFormat = DEFAULT_DATE_FORMAT;
|
||||||
public marketData: AdminMarketDataItem[] = [];
|
public marketData: AdminMarketDataItem[] = [];
|
||||||
@ -43,6 +44,19 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
|
|||||||
this.fetchAdminMarketData();
|
this.fetchAdminMarketData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onDeleteProfileData({
|
||||||
|
dataSource,
|
||||||
|
symbol
|
||||||
|
}: {
|
||||||
|
dataSource: DataSource;
|
||||||
|
symbol: string;
|
||||||
|
}) {
|
||||||
|
this.adminService
|
||||||
|
.deleteProfileData({ dataSource, symbol })
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
public onGatherProfileDataBySymbol({
|
public onGatherProfileDataBySymbol({
|
||||||
dataSource,
|
dataSource,
|
||||||
symbol
|
symbol
|
||||||
@ -69,22 +83,33 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
|
|||||||
.subscribe(() => {});
|
.subscribe(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
public setCurrentSymbol(aSymbol: string) {
|
|
||||||
this.marketDataDetails = [];
|
|
||||||
|
|
||||||
if (this.currentSymbol === aSymbol) {
|
|
||||||
this.currentSymbol = '';
|
|
||||||
} else {
|
|
||||||
this.currentSymbol = aSymbol;
|
|
||||||
|
|
||||||
this.fetchAdminMarketDataBySymbol(this.currentSymbol);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public onMarketDataChanged(withRefresh: boolean = false) {
|
public onMarketDataChanged(withRefresh: boolean = false) {
|
||||||
if (withRefresh) {
|
if (withRefresh) {
|
||||||
this.fetchAdminMarketData();
|
this.fetchAdminMarketData();
|
||||||
this.fetchAdminMarketDataBySymbol(this.currentSymbol);
|
this.fetchAdminMarketDataBySymbol({
|
||||||
|
dataSource: this.currentDataSource,
|
||||||
|
symbol: this.currentSymbol
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public setCurrentProfile({
|
||||||
|
dataSource,
|
||||||
|
symbol
|
||||||
|
}: {
|
||||||
|
dataSource: DataSource;
|
||||||
|
symbol: string;
|
||||||
|
}) {
|
||||||
|
this.marketDataDetails = [];
|
||||||
|
|
||||||
|
if (this.currentSymbol === symbol) {
|
||||||
|
this.currentDataSource = undefined;
|
||||||
|
this.currentSymbol = '';
|
||||||
|
} else {
|
||||||
|
this.currentDataSource = dataSource;
|
||||||
|
this.currentSymbol = symbol;
|
||||||
|
|
||||||
|
this.fetchAdminMarketDataBySymbol({ dataSource, symbol });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,9 +129,15 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private fetchAdminMarketDataBySymbol(aSymbol: string) {
|
private fetchAdminMarketDataBySymbol({
|
||||||
this.dataService
|
dataSource,
|
||||||
.fetchAdminMarketDataBySymbol(aSymbol)
|
symbol
|
||||||
|
}: {
|
||||||
|
dataSource: DataSource;
|
||||||
|
symbol: string;
|
||||||
|
}) {
|
||||||
|
this.adminService
|
||||||
|
.fetchAdminMarketDataBySymbol({ dataSource, symbol })
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(({ marketData }) => {
|
.subscribe(({ marketData }) => {
|
||||||
this.marketDataDetails = marketData;
|
this.marketDataDetails = marketData;
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
<ng-container *ngFor="let item of marketData; let i = index">
|
<ng-container *ngFor="let item of marketData; let i = index">
|
||||||
<tr
|
<tr
|
||||||
class="cursor-pointer mat-row"
|
class="cursor-pointer mat-row"
|
||||||
(click)="setCurrentSymbol(item.symbol)"
|
(click)="setCurrentProfile({ dataSource: item.dataSource, symbol: item.symbol })"
|
||||||
>
|
>
|
||||||
<td class="mat-cell px-1 py-2">{{ item.symbol }}</td>
|
<td class="mat-cell px-1 py-2">{{ item.symbol }}</td>
|
||||||
<td class="mat-cell px-1 py-2">{{ item.dataSource }}</td>
|
<td class="mat-cell px-1 py-2">{{ item.dataSource }}</td>
|
||||||
@ -49,11 +49,19 @@
|
|||||||
>
|
>
|
||||||
Gather Profile Data
|
Gather Profile Data
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
i18n
|
||||||
|
mat-menu-item
|
||||||
|
[disabled]="item.activityCount !== 0"
|
||||||
|
(click)="onDeleteProfileData({dataSource: item.dataSource, symbol: item.symbol})"
|
||||||
|
>
|
||||||
|
Delete Profile Data
|
||||||
|
</button>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr *ngIf="currentSymbol === item.symbol" class="mat-row">
|
<tr *ngIf="currentSymbol === item.symbol" class="mat-row">
|
||||||
<td class="p-1" colspan="4">
|
<td class="p-1" colspan="6">
|
||||||
<gf-admin-market-data-detail
|
<gf-admin-market-data-detail
|
||||||
[dataSource]="item.dataSource"
|
[dataSource]="item.dataSource"
|
||||||
[marketData]="marketDataDetails"
|
[marketData]="marketDataDetails"
|
||||||
|
@ -3,6 +3,7 @@ import { MatDialog } from '@angular/material/dialog';
|
|||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
|
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
|
||||||
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 {
|
import {
|
||||||
RANGE,
|
RANGE,
|
||||||
SettingsStorageService
|
SettingsStorageService
|
||||||
@ -26,6 +27,7 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
|||||||
public dateRange: DateRange;
|
public dateRange: DateRange;
|
||||||
public dateRangeOptions = defaultDateRangeOptions;
|
public dateRangeOptions = defaultDateRangeOptions;
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
|
public hasImpersonationId: boolean;
|
||||||
public hasPermissionToCreateOrder: boolean;
|
public hasPermissionToCreateOrder: boolean;
|
||||||
public positions: Position[];
|
public positions: Position[];
|
||||||
public user: User;
|
public user: User;
|
||||||
@ -40,6 +42,7 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
|||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
private deviceService: DeviceDetectorService,
|
private deviceService: DeviceDetectorService,
|
||||||
private dialog: MatDialog,
|
private dialog: MatDialog,
|
||||||
|
private impersonationStorageService: ImpersonationStorageService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private settingsStorageService: SettingsStorageService,
|
private settingsStorageService: SettingsStorageService,
|
||||||
@ -82,6 +85,13 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
|||||||
public ngOnInit() {
|
public ngOnInit() {
|
||||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||||
|
|
||||||
|
this.impersonationStorageService
|
||||||
|
.onChangeHasImpersonation()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((aId) => {
|
||||||
|
this.hasImpersonationId = !!aId;
|
||||||
|
});
|
||||||
|
|
||||||
this.dateRange =
|
this.dateRange =
|
||||||
<DateRange>this.settingsStorageService.getSetting(RANGE) || 'max';
|
<DateRange>this.settingsStorageService.getSetting(RANGE) || 'max';
|
||||||
|
|
||||||
@ -119,6 +129,7 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
|||||||
symbol,
|
symbol,
|
||||||
baseCurrency: this.user?.settings?.baseCurrency,
|
baseCurrency: this.user?.settings?.baseCurrency,
|
||||||
deviceType: this.deviceType,
|
deviceType: this.deviceType,
|
||||||
|
hasImpersonationId: this.hasImpersonationId,
|
||||||
locale: this.user?.settings?.locale
|
locale: this.user?.settings?.locale
|
||||||
},
|
},
|
||||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||||
|
@ -4,9 +4,8 @@ import { DataService } from '@ghostfolio/client/services/data.service';
|
|||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
import { ghostfolioFearAndGreedIndexSymbol } from '@ghostfolio/common/config';
|
import { ghostfolioFearAndGreedIndexSymbol } from '@ghostfolio/common/config';
|
||||||
import { resetHours } from '@ghostfolio/common/helper';
|
import { resetHours } from '@ghostfolio/common/helper';
|
||||||
import { User } from '@ghostfolio/common/interfaces';
|
import { InfoItem, User } from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { DataSource } from '@prisma/client';
|
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
@ -19,6 +18,7 @@ export class HomeMarketComponent implements OnDestroy, OnInit {
|
|||||||
public fearAndGreedIndex: number;
|
public fearAndGreedIndex: number;
|
||||||
public hasPermissionToAccessFearAndGreedIndex: boolean;
|
public hasPermissionToAccessFearAndGreedIndex: boolean;
|
||||||
public historicalData: HistoricalDataItem[];
|
public historicalData: HistoricalDataItem[];
|
||||||
|
public info: InfoItem;
|
||||||
public isLoading = true;
|
public isLoading = true;
|
||||||
public readonly numberOfDays = 90;
|
public readonly numberOfDays = 90;
|
||||||
public user: User;
|
public user: User;
|
||||||
@ -33,6 +33,7 @@ export class HomeMarketComponent implements OnDestroy, OnInit {
|
|||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
private userService: UserService
|
private userService: UserService
|
||||||
) {
|
) {
|
||||||
|
this.info = this.dataService.fetchInfo();
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
|
||||||
this.userService.stateChanged
|
this.userService.stateChanged
|
||||||
@ -49,7 +50,7 @@ export class HomeMarketComponent implements OnDestroy, OnInit {
|
|||||||
if (this.hasPermissionToAccessFearAndGreedIndex) {
|
if (this.hasPermissionToAccessFearAndGreedIndex) {
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchSymbolItem({
|
.fetchSymbolItem({
|
||||||
dataSource: DataSource.RAKUTEN,
|
dataSource: this.info.fearAndGreedDataSource,
|
||||||
includeHistoricalData: this.numberOfDays,
|
includeHistoricalData: this.numberOfDays,
|
||||||
symbol: ghostfolioFearAndGreedIndexSymbol
|
symbol: ghostfolioFearAndGreedIndexSymbol
|
||||||
})
|
})
|
||||||
|
@ -4,6 +4,7 @@ export interface PositionDetailDialogParams {
|
|||||||
baseCurrency: string;
|
baseCurrency: string;
|
||||||
dataSource: DataSource;
|
dataSource: DataSource;
|
||||||
deviceType: string;
|
deviceType: string;
|
||||||
|
hasImpersonationId: boolean;
|
||||||
locale: string;
|
locale: string;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper';
|
||||||
import { OrderWithAccount } from '@ghostfolio/common/types';
|
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||||
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
|
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
|
||||||
import { AssetSubClass } from '@prisma/client';
|
import { AssetSubClass } from '@prisma/client';
|
||||||
@ -185,6 +185,26 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
|||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onExport() {
|
||||||
|
this.dataService
|
||||||
|
.fetchExport(
|
||||||
|
this.orders.map((order) => {
|
||||||
|
return order.id;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((data) => {
|
||||||
|
downloadAsFile(
|
||||||
|
data,
|
||||||
|
`ghostfolio-export-${this.symbol}-${format(
|
||||||
|
parseISO(data.meta.date),
|
||||||
|
'yyyyMMddHHmm'
|
||||||
|
)}.json`,
|
||||||
|
'text/plain'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
this.unsubscribeSubject.next();
|
this.unsubscribeSubject.next();
|
||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
|
@ -131,12 +131,14 @@
|
|||||||
[baseCurrency]="data.baseCurrency"
|
[baseCurrency]="data.baseCurrency"
|
||||||
[deviceType]="data.deviceType"
|
[deviceType]="data.deviceType"
|
||||||
[hasPermissionToCreateActivity]="false"
|
[hasPermissionToCreateActivity]="false"
|
||||||
|
[hasPermissionToExportActivities]="!hasImpersonationId"
|
||||||
[hasPermissionToFilter]="false"
|
[hasPermissionToFilter]="false"
|
||||||
[hasPermissionToImportActivities]="false"
|
[hasPermissionToImportActivities]="false"
|
||||||
[hasPermissionToOpenDetails]="false"
|
[hasPermissionToOpenDetails]="false"
|
||||||
[locale]="data.locale"
|
[locale]="data.locale"
|
||||||
[showActions]="false"
|
[showActions]="false"
|
||||||
[showSymbolColumn]="false"
|
[showSymbolColumn]="false"
|
||||||
|
(export)="onExport()"
|
||||||
></gf-activities-table>
|
></gf-activities-table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'mb-5' },
|
host: { class: 'page' },
|
||||||
selector: 'gf-about-page',
|
selector: 'gf-about-page',
|
||||||
styleUrls: ['./about-page.scss'],
|
styleUrls: ['./about-page.scss'],
|
||||||
templateUrl: './about-page.html'
|
templateUrl: './about-page.html'
|
||||||
|
@ -2,7 +2,7 @@ import { Component, OnDestroy } from '@angular/core';
|
|||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'mb-5' },
|
host: { class: 'page' },
|
||||||
selector: 'gf-changelog-page',
|
selector: 'gf-changelog-page',
|
||||||
styleUrls: ['./changelog-page.scss'],
|
styleUrls: ['./changelog-page.scss'],
|
||||||
templateUrl: './changelog-page.html'
|
templateUrl: './changelog-page.html'
|
||||||
|
@ -31,7 +31,7 @@ import { catchError, switchMap, takeUntil } from 'rxjs/operators';
|
|||||||
import { CreateOrUpdateAccessDialog } from './create-or-update-access-dialog/create-or-update-access-dialog.component';
|
import { CreateOrUpdateAccessDialog } from './create-or-update-access-dialog/create-or-update-access-dialog.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'mb-5' },
|
host: { class: 'page' },
|
||||||
selector: 'gf-account-page',
|
selector: 'gf-account-page',
|
||||||
styleUrls: ['./account-page.scss'],
|
styleUrls: ['./account-page.scss'],
|
||||||
templateUrl: './account-page.html'
|
templateUrl: './account-page.html'
|
||||||
|
@ -16,7 +16,7 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog/create-or-update-account-dialog.component';
|
import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog/create-or-update-account-dialog.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'mb-5' },
|
host: { class: 'page' },
|
||||||
selector: 'gf-accounts-page',
|
selector: 'gf-accounts-page',
|
||||||
styleUrls: ['./accounts-page.scss'],
|
styleUrls: ['./accounts-page.scss'],
|
||||||
templateUrl: './accounts-page.html'
|
templateUrl: './accounts-page.html'
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row mb-3">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h3 class="d-flex justify-content-center mb-3" i18n>Accounts</h3>
|
<h3 class="d-flex justify-content-center mb-3" i18n>Accounts</h3>
|
||||||
|
<div class="accounts">
|
||||||
<gf-accounts-table
|
<gf-accounts-table
|
||||||
[accounts]="accounts"
|
[accounts]="accounts"
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
@ -16,6 +17,7 @@
|
|||||||
></gf-accounts-table>
|
></gf-accounts-table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
*ngIf="!hasImpersonationId && hasPermissionToCreateAccount && !user.settings.isRestrictedView"
|
*ngIf="!hasImpersonationId && hasPermissionToCreateAccount && !user.settings.isRestrictedView"
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
|
.accounts {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.fab-container {
|
.fab-container {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 2rem;
|
right: 2rem;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'mb-5' },
|
host: { class: 'page' },
|
||||||
selector: 'gf-hallo-ghostfolio-page',
|
selector: 'gf-hallo-ghostfolio-page',
|
||||||
styleUrls: ['./hallo-ghostfolio-page.scss'],
|
styleUrls: ['./hallo-ghostfolio-page.scss'],
|
||||||
templateUrl: './hallo-ghostfolio-page.html'
|
templateUrl: './hallo-ghostfolio-page.html'
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'mb-5' },
|
host: { class: 'page' },
|
||||||
selector: 'gf-hello-ghostfolio-page',
|
selector: 'gf-hello-ghostfolio-page',
|
||||||
styleUrls: ['./hello-ghostfolio-page.scss'],
|
styleUrls: ['./hello-ghostfolio-page.scss'],
|
||||||
templateUrl: './hello-ghostfolio-page.html'
|
templateUrl: './hello-ghostfolio-page.html'
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'mb-5' },
|
host: { class: 'page' },
|
||||||
selector: 'gf-first-months-in-open-source-page',
|
selector: 'gf-first-months-in-open-source-page',
|
||||||
styleUrls: ['./first-months-in-open-source-page.scss'],
|
styleUrls: ['./first-months-in-open-source-page.scss'],
|
||||||
templateUrl: './first-months-in-open-source-page.html'
|
templateUrl: './first-months-in-open-source-page.html'
|
||||||
|
@ -2,7 +2,7 @@ import { Component, OnDestroy } from '@angular/core';
|
|||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'mb-5' },
|
host: { class: 'page' },
|
||||||
selector: 'gf-blog-page',
|
selector: 'gf-blog-page',
|
||||||
styleUrls: ['./blog-page.scss'],
|
styleUrls: ['./blog-page.scss'],
|
||||||
templateUrl: './blog-page.html'
|
templateUrl: './blog-page.html'
|
||||||
|
@ -6,7 +6,7 @@ import { format } from 'date-fns';
|
|||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'mb-5' },
|
host: { class: 'page' },
|
||||||
selector: 'gf-landing-page',
|
selector: 'gf-landing-page',
|
||||||
styleUrls: ['./landing-page.scss'],
|
styleUrls: ['./landing-page.scss'],
|
||||||
templateUrl: './landing-page.html'
|
templateUrl: './landing-page.html'
|
||||||
|
@ -20,7 +20,7 @@ import { Subject, Subscription } from 'rxjs';
|
|||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'mb-5' },
|
host: { class: 'page' },
|
||||||
selector: 'gf-allocations-page',
|
selector: 'gf-allocations-page',
|
||||||
styleUrls: ['./allocations-page.scss'],
|
styleUrls: ['./allocations-page.scss'],
|
||||||
templateUrl: './allocations-page.html'
|
templateUrl: './allocations-page.html'
|
||||||
@ -316,6 +316,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
symbol,
|
symbol,
|
||||||
baseCurrency: this.user?.settings?.baseCurrency,
|
baseCurrency: this.user?.settings?.baseCurrency,
|
||||||
deviceType: this.deviceType,
|
deviceType: this.deviceType,
|
||||||
|
hasImpersonationId: this.hasImpersonationId,
|
||||||
locale: this.user?.settings?.locale
|
locale: this.user?.settings?.locale
|
||||||
},
|
},
|
||||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||||
|
@ -11,7 +11,7 @@ import { Subject } from 'rxjs';
|
|||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'mb-5' },
|
host: { class: 'page' },
|
||||||
selector: 'gf-analysis-page',
|
selector: 'gf-analysis-page',
|
||||||
styleUrls: ['./analysis-page.scss'],
|
styleUrls: ['./analysis-page.scss'],
|
||||||
templateUrl: './analysis-page.html'
|
templateUrl: './analysis-page.html'
|
||||||
|
@ -7,7 +7,7 @@ import { Subject } from 'rxjs';
|
|||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'mb-5' },
|
host: { class: 'page' },
|
||||||
selector: 'gf-portfolio-page',
|
selector: 'gf-portfolio-page',
|
||||||
styleUrls: ['./portfolio-page.scss'],
|
styleUrls: ['./portfolio-page.scss'],
|
||||||
templateUrl: './portfolio-page.html'
|
templateUrl: './portfolio-page.html'
|
||||||
|
@ -7,7 +7,7 @@ import { Subject } from 'rxjs';
|
|||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'mb-5' },
|
host: { class: 'page' },
|
||||||
selector: 'gf-report-page',
|
selector: 'gf-report-page',
|
||||||
styleUrls: ['./report-page.scss'],
|
styleUrls: ['./report-page.scss'],
|
||||||
templateUrl: './report-page.html'
|
templateUrl: './report-page.html'
|
||||||
|
@ -10,6 +10,7 @@ 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 { ImportTransactionsService } from '@ghostfolio/client/services/import-transactions.service';
|
import { ImportTransactionsService } from '@ghostfolio/client/services/import-transactions.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
|
import { downloadAsFile } from '@ghostfolio/common/helper';
|
||||||
import { User } from '@ghostfolio/common/interfaces';
|
import { User } from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { DataSource, Order as OrderModel } from '@prisma/client';
|
import { DataSource, Order as OrderModel } from '@prisma/client';
|
||||||
@ -23,7 +24,7 @@ import { CreateOrUpdateTransactionDialog } from './create-or-update-transaction-
|
|||||||
import { ImportTransactionDialog } from './import-transaction-dialog/import-transaction-dialog.component';
|
import { ImportTransactionDialog } from './import-transaction-dialog/import-transaction-dialog.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'mb-5' },
|
host: { class: 'page' },
|
||||||
selector: 'gf-transactions-page',
|
selector: 'gf-transactions-page',
|
||||||
styleUrls: ['./transactions-page.scss'],
|
styleUrls: ['./transactions-page.scss'],
|
||||||
templateUrl: './transactions-page.html'
|
templateUrl: './transactions-page.html'
|
||||||
@ -90,11 +91,6 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
|||||||
public ngOnInit() {
|
public ngOnInit() {
|
||||||
const { globalPermissions } = this.dataService.fetchInfo();
|
const { globalPermissions } = this.dataService.fetchInfo();
|
||||||
|
|
||||||
this.hasPermissionToImportOrders = hasPermission(
|
|
||||||
globalPermissions,
|
|
||||||
permissions.enableImport
|
|
||||||
);
|
|
||||||
|
|
||||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||||
|
|
||||||
this.impersonationStorageService
|
this.impersonationStorageService
|
||||||
@ -102,6 +98,10 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
|||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((aId) => {
|
.subscribe((aId) => {
|
||||||
this.hasImpersonationId = !!aId;
|
this.hasImpersonationId = !!aId;
|
||||||
|
|
||||||
|
this.hasPermissionToImportOrders =
|
||||||
|
hasPermission(globalPermissions, permissions.enableImport) &&
|
||||||
|
!this.hasImpersonationId;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.userService.stateChanged
|
this.userService.stateChanged
|
||||||
@ -147,12 +147,12 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onExport() {
|
public onExport(activityIds?: string[]) {
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchExport()
|
.fetchExport(activityIds)
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((data) => {
|
.subscribe((data) => {
|
||||||
this.downloadAsFile(
|
downloadAsFile(
|
||||||
data,
|
data,
|
||||||
`ghostfolio-export-${format(
|
`ghostfolio-export-${format(
|
||||||
parseISO(data.meta.date),
|
parseISO(data.meta.date),
|
||||||
@ -303,20 +303,6 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
|||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
private downloadAsFile(
|
|
||||||
aContent: unknown,
|
|
||||||
aFileName: string,
|
|
||||||
aContentType: string
|
|
||||||
) {
|
|
||||||
const a = document.createElement('a');
|
|
||||||
const file = new Blob([JSON.stringify(aContent, undefined, ' ')], {
|
|
||||||
type: aContentType
|
|
||||||
});
|
|
||||||
a.href = URL.createObjectURL(file);
|
|
||||||
a.download = aFileName;
|
|
||||||
a.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleImportError({ error, orders }: { error: any; orders: any[] }) {
|
private handleImportError({ error, orders }: { error: any; orders: any[] }) {
|
||||||
this.snackBar.dismiss();
|
this.snackBar.dismiss();
|
||||||
|
|
||||||
@ -406,6 +392,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
|||||||
symbol,
|
symbol,
|
||||||
baseCurrency: this.user?.settings?.baseCurrency,
|
baseCurrency: this.user?.settings?.baseCurrency,
|
||||||
deviceType: this.deviceType,
|
deviceType: this.deviceType,
|
||||||
|
hasImpersonationId: this.hasImpersonationId,
|
||||||
locale: this.user?.settings?.locale
|
locale: this.user?.settings?.locale
|
||||||
},
|
},
|
||||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||||
|
@ -7,13 +7,14 @@
|
|||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
[deviceType]="deviceType"
|
[deviceType]="deviceType"
|
||||||
[hasPermissionToCreateActivity]="hasPermissionToCreateOrder"
|
[hasPermissionToCreateActivity]="hasPermissionToCreateOrder"
|
||||||
|
[hasPermissionToExportActivities]="!hasImpersonationId"
|
||||||
[hasPermissionToImportActivities]="hasPermissionToImportOrders"
|
[hasPermissionToImportActivities]="hasPermissionToImportOrders"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[showActions]="!hasImpersonationId && hasPermissionToDeleteOrder && !user.settings.isRestrictedView"
|
[showActions]="!hasImpersonationId && hasPermissionToDeleteOrder && !user.settings.isRestrictedView"
|
||||||
(activityDeleted)="onDeleteTransaction($event)"
|
(activityDeleted)="onDeleteTransaction($event)"
|
||||||
(activityToClone)="onCloneTransaction($event)"
|
(activityToClone)="onCloneTransaction($event)"
|
||||||
(activityToUpdate)="onUpdateTransaction($event)"
|
(activityToUpdate)="onUpdateTransaction($event)"
|
||||||
(export)="onExport()"
|
(export)="onExport($event)"
|
||||||
(import)="onImport()"
|
(import)="onImport()"
|
||||||
></gf-activities-table>
|
></gf-activities-table>
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,7 +7,7 @@ import { Subject } from 'rxjs';
|
|||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'mb-5' },
|
host: { class: 'page' },
|
||||||
selector: 'gf-pricing-page',
|
selector: 'gf-pricing-page',
|
||||||
styleUrls: ['./pricing-page.scss'],
|
styleUrls: ['./pricing-page.scss'],
|
||||||
templateUrl: './pricing-page.html'
|
templateUrl: './pricing-page.html'
|
||||||
|
@ -13,7 +13,7 @@ import { EMPTY, Subject } from 'rxjs';
|
|||||||
import { catchError, takeUntil } from 'rxjs/operators';
|
import { catchError, takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'mb-5' },
|
host: { class: 'page' },
|
||||||
selector: 'gf-public-page',
|
selector: 'gf-public-page',
|
||||||
styleUrls: ['./public-page.scss'],
|
styleUrls: ['./public-page.scss'],
|
||||||
templateUrl: './public-page.html'
|
templateUrl: './public-page.html'
|
||||||
|
@ -14,7 +14,7 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
import { ShowAccessTokenDialog } from './show-access-token-dialog/show-access-token-dialog.component';
|
import { ShowAccessTokenDialog } from './show-access-token-dialog/show-access-token-dialog.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'mb-5' },
|
host: { class: 'page' },
|
||||||
selector: 'gf-register-page',
|
selector: 'gf-register-page',
|
||||||
styleUrls: ['./register-page.scss'],
|
styleUrls: ['./register-page.scss'],
|
||||||
templateUrl: './register-page.html'
|
templateUrl: './register-page.html'
|
||||||
|
@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core';
|
|||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'mb-5' },
|
host: { class: 'page' },
|
||||||
selector: 'gf-resources-page',
|
selector: 'gf-resources-page',
|
||||||
styleUrls: ['./resources-page.scss'],
|
styleUrls: ['./resources-page.scss'],
|
||||||
templateUrl: './resources-page.html'
|
templateUrl: './resources-page.html'
|
||||||
|
@ -6,7 +6,7 @@ import { Subject } from 'rxjs';
|
|||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
host: { class: 'mb-5' },
|
host: { class: 'page' },
|
||||||
selector: 'gf-webauthn-page',
|
selector: 'gf-webauthn-page',
|
||||||
styleUrls: ['./webauthn-page.scss'],
|
styleUrls: ['./webauthn-page.scss'],
|
||||||
templateUrl: './webauthn-page.html'
|
templateUrl: './webauthn-page.html'
|
||||||
|
@ -3,8 +3,10 @@ import { Injectable } from '@angular/core';
|
|||||||
import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-data.dto';
|
import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-data.dto';
|
||||||
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
|
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||||
|
import { AdminMarketDataDetails } from '@ghostfolio/common/interfaces';
|
||||||
import { DataSource, MarketData } from '@prisma/client';
|
import { DataSource, MarketData } from '@prisma/client';
|
||||||
import { format } from 'date-fns';
|
import { format, parseISO } from 'date-fns';
|
||||||
|
import { map, Observable } from 'rxjs';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@ -12,6 +14,37 @@ import { format } from 'date-fns';
|
|||||||
export class AdminService {
|
export class AdminService {
|
||||||
public constructor(private http: HttpClient) {}
|
public constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
public deleteProfileData({
|
||||||
|
dataSource,
|
||||||
|
symbol
|
||||||
|
}: {
|
||||||
|
dataSource: DataSource;
|
||||||
|
symbol: string;
|
||||||
|
}) {
|
||||||
|
return this.http.delete<void>(
|
||||||
|
`/api/admin/profile-data/${dataSource}/${symbol}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public fetchAdminMarketDataBySymbol({
|
||||||
|
dataSource,
|
||||||
|
symbol
|
||||||
|
}: {
|
||||||
|
dataSource: DataSource;
|
||||||
|
symbol: string;
|
||||||
|
}): Observable<AdminMarketDataDetails> {
|
||||||
|
return this.http
|
||||||
|
.get<any>(`/api/admin/market-data/${dataSource}/${symbol}`)
|
||||||
|
.pipe(
|
||||||
|
map((data) => {
|
||||||
|
for (const item of data.marketData) {
|
||||||
|
item.date = parseISO(item.date);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public gatherMax() {
|
public gatherMax() {
|
||||||
return this.http.post<void>(`/api/admin/gather/max`, {});
|
return this.http.post<void>(`/api/admin/gather/max`, {});
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,6 @@ import {
|
|||||||
Accounts,
|
Accounts,
|
||||||
AdminData,
|
AdminData,
|
||||||
AdminMarketData,
|
AdminMarketData,
|
||||||
AdminMarketDataDetails,
|
|
||||||
Export,
|
Export,
|
||||||
InfoItem,
|
InfoItem,
|
||||||
PortfolioChart,
|
PortfolioChart,
|
||||||
@ -69,19 +68,6 @@ export class DataService {
|
|||||||
return this.http.get<AdminMarketData>('/api/admin/market-data');
|
return this.http.get<AdminMarketData>('/api/admin/market-data');
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchAdminMarketDataBySymbol(
|
|
||||||
aSymbol: string
|
|
||||||
): Observable<AdminMarketDataDetails> {
|
|
||||||
return this.http.get<any>(`/api/admin/market-data/${aSymbol}`).pipe(
|
|
||||||
map((data) => {
|
|
||||||
for (const item of data.marketData) {
|
|
||||||
item.date = parseISO(item.date);
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public deleteAccess(aId: string) {
|
public deleteAccess(aId: string) {
|
||||||
return this.http.delete<any>(`/api/access/${aId}`);
|
return this.http.delete<any>(`/api/access/${aId}`);
|
||||||
}
|
}
|
||||||
@ -108,8 +94,16 @@ export class DataService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchExport() {
|
public fetchExport(activityIds?: string[]) {
|
||||||
return this.http.get<Export>('/api/export');
|
let params = new HttpParams();
|
||||||
|
|
||||||
|
if (activityIds) {
|
||||||
|
params = params.append('activityIds', activityIds.join(','));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.http.get<Export>('/api/export', {
|
||||||
|
params
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchInfo(): InfoItem {
|
public fetchInfo(): InfoItem {
|
||||||
@ -141,7 +135,7 @@ export class DataService {
|
|||||||
includeHistoricalData,
|
includeHistoricalData,
|
||||||
symbol
|
symbol
|
||||||
}: {
|
}: {
|
||||||
dataSource: DataSource;
|
dataSource: DataSource | string;
|
||||||
includeHistoricalData?: number;
|
includeHistoricalData?: number;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
}) {
|
}) {
|
||||||
|
@ -164,6 +164,10 @@ ngx-skeleton-loader {
|
|||||||
min-width: unset !important;
|
min-width: unset !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
padding-bottom: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.svgMap-tooltip {
|
.svgMap-tooltip {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import * as currencies from '@dinero.js/currencies';
|
import * as currencies from '@dinero.js/currencies';
|
||||||
|
import { DataSource } from '@prisma/client';
|
||||||
import { getDate, getMonth, getYear, parse, subDays } from 'date-fns';
|
import { getDate, getMonth, getYear, parse, subDays } from 'date-fns';
|
||||||
|
|
||||||
import { ghostfolioScraperApiSymbolPrefix } from './config';
|
import { ghostfolioScraperApiSymbolPrefix } from './config';
|
||||||
@ -7,6 +8,28 @@ export function capitalize(aString: string) {
|
|||||||
return aString.charAt(0).toUpperCase() + aString.slice(1).toLowerCase();
|
return aString.charAt(0).toUpperCase() + aString.slice(1).toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function decodeDataSource(encodedDataSource: string) {
|
||||||
|
return Buffer.from(encodedDataSource, 'hex').toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function downloadAsFile(
|
||||||
|
aContent: unknown,
|
||||||
|
aFileName: string,
|
||||||
|
aContentType: string
|
||||||
|
) {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
const file = new Blob([JSON.stringify(aContent, undefined, ' ')], {
|
||||||
|
type: aContentType
|
||||||
|
});
|
||||||
|
a.href = URL.createObjectURL(file);
|
||||||
|
a.download = aFileName;
|
||||||
|
a.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encodeDataSource(aDataSource: DataSource) {
|
||||||
|
return Buffer.from(aDataSource, 'utf-8').toString('hex');
|
||||||
|
}
|
||||||
|
|
||||||
export function getBackgroundColor() {
|
export function getBackgroundColor() {
|
||||||
return getCssVariable(
|
return getCssVariable(
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').matches
|
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
|
@ -4,6 +4,7 @@ import { Subscription } from './subscription.interface';
|
|||||||
export interface InfoItem {
|
export interface InfoItem {
|
||||||
currencies: string[];
|
currencies: string[];
|
||||||
demoAuthToken: string;
|
demoAuthToken: string;
|
||||||
|
fearAndGreedDataSource?: string;
|
||||||
globalPermissions: string[];
|
globalPermissions: string[];
|
||||||
isReadOnlyMode?: boolean;
|
isReadOnlyMode?: boolean;
|
||||||
lastDataGathering?: Date;
|
lastDataGathering?: Date;
|
||||||
|
@ -36,14 +36,15 @@
|
|||||||
</mat-autocomplete>
|
</mat-autocomplete>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<table
|
<div class="activities">
|
||||||
|
<table
|
||||||
class="gf-table w-100"
|
class="gf-table w-100"
|
||||||
matSort
|
matSort
|
||||||
matSortActive="date"
|
matSortActive="date"
|
||||||
matSortDirection="desc"
|
matSortDirection="desc"
|
||||||
mat-table
|
mat-table
|
||||||
[dataSource]="dataSource"
|
[dataSource]="dataSource"
|
||||||
>
|
>
|
||||||
<ng-container matColumnDef="count">
|
<ng-container matColumnDef="count">
|
||||||
<th
|
<th
|
||||||
*matHeaderCellDef
|
*matHeaderCellDef
|
||||||
@ -127,7 +128,11 @@
|
|||||||
>
|
>
|
||||||
Currency
|
Currency
|
||||||
</th>
|
</th>
|
||||||
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
|
<td
|
||||||
|
*matCellDef="let element"
|
||||||
|
class="d-none d-lg-table-cell px-1"
|
||||||
|
mat-cell
|
||||||
|
>
|
||||||
{{ element.currency }}
|
{{ element.currency }}
|
||||||
</td>
|
</td>
|
||||||
<td *matFooterCellDef class="d-none d-lg-table-cell px-1" mat-footer-cell>
|
<td *matFooterCellDef class="d-none d-lg-table-cell px-1" mat-footer-cell>
|
||||||
@ -145,7 +150,11 @@
|
|||||||
>
|
>
|
||||||
Quantity
|
Quantity
|
||||||
</th>
|
</th>
|
||||||
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
|
<td
|
||||||
|
*matCellDef="let element"
|
||||||
|
class="d-none d-lg-table-cell px-1"
|
||||||
|
mat-cell
|
||||||
|
>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<gf-value
|
<gf-value
|
||||||
[isCurrency]="true"
|
[isCurrency]="true"
|
||||||
@ -171,7 +180,11 @@
|
|||||||
>
|
>
|
||||||
Unit Price
|
Unit Price
|
||||||
</th>
|
</th>
|
||||||
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
|
<td
|
||||||
|
*matCellDef="let element"
|
||||||
|
class="d-none d-lg-table-cell px-1"
|
||||||
|
mat-cell
|
||||||
|
>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<gf-value
|
<gf-value
|
||||||
[isCurrency]="true"
|
[isCurrency]="true"
|
||||||
@ -197,7 +210,11 @@
|
|||||||
>
|
>
|
||||||
Fee
|
Fee
|
||||||
</th>
|
</th>
|
||||||
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
|
<td
|
||||||
|
*matCellDef="let element"
|
||||||
|
class="d-none d-lg-table-cell px-1"
|
||||||
|
mat-cell
|
||||||
|
>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<gf-value
|
<gf-value
|
||||||
[isCurrency]="true"
|
[isCurrency]="true"
|
||||||
@ -239,6 +256,7 @@
|
|||||||
<td *matFooterCellDef class="px-1" mat-footer-cell>
|
<td *matFooterCellDef class="px-1" mat-footer-cell>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<gf-value
|
<gf-value
|
||||||
|
[isAbsolute]="true"
|
||||||
[isCurrency]="true"
|
[isCurrency]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[value]="isLoading ? undefined : totalValue"
|
[value]="isLoading ? undefined : totalValue"
|
||||||
@ -268,6 +286,9 @@
|
|||||||
<ng-container matColumnDef="actions">
|
<ng-container matColumnDef="actions">
|
||||||
<th *matHeaderCellDef class="px-1 text-center" mat-header-cell>
|
<th *matHeaderCellDef class="px-1 text-center" mat-header-cell>
|
||||||
<button
|
<button
|
||||||
|
*ngIf="
|
||||||
|
hasPermissionToExportActivities || hasPermissionToImportActivities
|
||||||
|
"
|
||||||
class="mx-1 no-min-width px-2"
|
class="mx-1 no-min-width px-2"
|
||||||
mat-button
|
mat-button
|
||||||
[matMenuTriggerFor]="activitiesMenu"
|
[matMenuTriggerFor]="activitiesMenu"
|
||||||
@ -286,6 +307,7 @@
|
|||||||
<span i18n>Import</span>
|
<span i18n>Import</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
*ngIf="hasPermissionToExportActivities"
|
||||||
class="align-items-center d-flex"
|
class="align-items-center d-flex"
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
(click)="onExport()"
|
(click)="onExport()"
|
||||||
@ -297,6 +319,7 @@
|
|||||||
</th>
|
</th>
|
||||||
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
||||||
<button
|
<button
|
||||||
|
*ngIf="this.showActions"
|
||||||
class="mx-1 no-min-width px-2"
|
class="mx-1 no-min-width px-2"
|
||||||
mat-button
|
mat-button
|
||||||
[matMenuTriggerFor]="activityMenu"
|
[matMenuTriggerFor]="activityMenu"
|
||||||
@ -331,14 +354,17 @@
|
|||||||
symbol: row.symbol
|
symbol: row.symbol
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
[ngClass]="{ 'cursor-pointer': hasPermissionToOpenDetails && !row.isDraft }"
|
[ngClass]="{
|
||||||
|
'cursor-pointer': hasPermissionToOpenDetails && !row.isDraft
|
||||||
|
}"
|
||||||
></tr>
|
></tr>
|
||||||
<tr
|
<tr
|
||||||
*matFooterRowDef="displayedColumns"
|
*matFooterRowDef="displayedColumns"
|
||||||
mat-footer-row
|
mat-footer-row
|
||||||
[ngClass]="{ 'd-none': isLoading || dataSource.data.length === 0 }"
|
[ngClass]="{ 'd-none': isLoading || dataSource.data.length === 0 }"
|
||||||
></tr>
|
></tr>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ngx-skeleton-loader
|
<ngx-skeleton-loader
|
||||||
*ngIf="isLoading"
|
*ngIf="isLoading"
|
||||||
|
@ -14,6 +14,9 @@
|
|||||||
min-height: 1.5rem !important;
|
min-height: 1.5rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.activities {
|
||||||
|
overflow-x: auto;
|
||||||
|
|
||||||
.mat-table {
|
.mat-table {
|
||||||
td {
|
td {
|
||||||
&.mat-footer-cell {
|
&.mat-footer-cell {
|
||||||
@ -57,6 +60,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:host-context(.is-dark-theme) {
|
:host-context(.is-dark-theme) {
|
||||||
|
@ -43,6 +43,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
|||||||
@Input() baseCurrency: string;
|
@Input() baseCurrency: string;
|
||||||
@Input() deviceType: string;
|
@Input() deviceType: string;
|
||||||
@Input() hasPermissionToCreateActivity: boolean;
|
@Input() hasPermissionToCreateActivity: boolean;
|
||||||
|
@Input() hasPermissionToExportActivities: boolean;
|
||||||
@Input() hasPermissionToFilter = true;
|
@Input() hasPermissionToFilter = true;
|
||||||
@Input() hasPermissionToImportActivities: boolean;
|
@Input() hasPermissionToImportActivities: boolean;
|
||||||
@Input() hasPermissionToOpenDetails = true;
|
@Input() hasPermissionToOpenDetails = true;
|
||||||
@ -53,7 +54,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
|||||||
@Output() activityDeleted = new EventEmitter<string>();
|
@Output() activityDeleted = new EventEmitter<string>();
|
||||||
@Output() activityToClone = new EventEmitter<OrderWithAccount>();
|
@Output() activityToClone = new EventEmitter<OrderWithAccount>();
|
||||||
@Output() activityToUpdate = new EventEmitter<OrderWithAccount>();
|
@Output() activityToUpdate = new EventEmitter<OrderWithAccount>();
|
||||||
@Output() export = new EventEmitter<void>();
|
@Output() export = new EventEmitter<string[]>();
|
||||||
@Output() import = new EventEmitter<void>();
|
@Output() import = new EventEmitter<void>();
|
||||||
|
|
||||||
@ViewChild('autocomplete') matAutocomplete: MatAutocomplete;
|
@ViewChild('autocomplete') matAutocomplete: MatAutocomplete;
|
||||||
@ -132,18 +133,15 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
|||||||
'date',
|
'date',
|
||||||
'type',
|
'type',
|
||||||
'symbol',
|
'symbol',
|
||||||
'currency',
|
|
||||||
'quantity',
|
'quantity',
|
||||||
|
'currency',
|
||||||
'unitPrice',
|
'unitPrice',
|
||||||
'fee',
|
'fee',
|
||||||
'value',
|
'value',
|
||||||
'account'
|
'account',
|
||||||
|
'actions'
|
||||||
];
|
];
|
||||||
|
|
||||||
if (this.showActions) {
|
|
||||||
this.displayedColumns.push('actions');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.showSymbolColumn) {
|
if (!this.showSymbolColumn) {
|
||||||
this.displayedColumns = this.displayedColumns.filter((column) => {
|
this.displayedColumns = this.displayedColumns.filter((column) => {
|
||||||
return column !== 'symbol';
|
return column !== 'symbol';
|
||||||
@ -184,8 +182,16 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onExport() {
|
public onExport() {
|
||||||
|
if (this.searchKeywords.length > 0) {
|
||||||
|
this.export.emit(
|
||||||
|
this.dataSource.filteredData.map((activity) => {
|
||||||
|
return activity.id;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
this.export.emit();
|
this.export.emit();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public onImport() {
|
public onImport() {
|
||||||
this.import.emit();
|
this.import.emit();
|
||||||
|
@ -17,6 +17,7 @@ import { isNumber } from 'lodash';
|
|||||||
export class ValueComponent implements OnChanges {
|
export class ValueComponent implements OnChanges {
|
||||||
@Input() colorizeSign = false;
|
@Input() colorizeSign = false;
|
||||||
@Input() currency = '';
|
@Input() currency = '';
|
||||||
|
@Input() isAbsolute = false;
|
||||||
@Input() isCurrency = false;
|
@Input() isCurrency = false;
|
||||||
@Input() isPercent = false;
|
@Input() isPercent = false;
|
||||||
@Input() label = '';
|
@Input() label = '';
|
||||||
@ -91,6 +92,11 @@ export class ValueComponent implements OnChanges {
|
|||||||
} else {
|
} else {
|
||||||
this.formattedValue = this.value?.toString();
|
this.formattedValue = this.value?.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.isAbsolute) {
|
||||||
|
// Remove algebraic sign
|
||||||
|
this.formattedValue = this.formattedValue.replace(/^-/, '');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
if (isDate(new Date(this.value))) {
|
if (isDate(new Date(this.value))) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ghostfolio",
|
"name": "ghostfolio",
|
||||||
"version": "1.109.0",
|
"version": "1.112.0",
|
||||||
"homepage": "https://ghostfol.io",
|
"homepage": "https://ghostfol.io",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -70,7 +70,7 @@
|
|||||||
"@nestjs/schedule": "1.0.2",
|
"@nestjs/schedule": "1.0.2",
|
||||||
"@nestjs/serve-static": "2.2.2",
|
"@nestjs/serve-static": "2.2.2",
|
||||||
"@nrwl/angular": "13.4.1",
|
"@nrwl/angular": "13.4.1",
|
||||||
"@prisma/client": "3.8.1",
|
"@prisma/client": "3.9.1",
|
||||||
"@simplewebauthn/browser": "4.1.0",
|
"@simplewebauthn/browser": "4.1.0",
|
||||||
"@simplewebauthn/server": "4.1.0",
|
"@simplewebauthn/server": "4.1.0",
|
||||||
"@simplewebauthn/typescript-types": "4.0.0",
|
"@simplewebauthn/typescript-types": "4.0.0",
|
||||||
@ -107,7 +107,7 @@
|
|||||||
"passport": "0.4.1",
|
"passport": "0.4.1",
|
||||||
"passport-google-oauth20": "2.0.0",
|
"passport-google-oauth20": "2.0.0",
|
||||||
"passport-jwt": "4.0.0",
|
"passport-jwt": "4.0.0",
|
||||||
"prisma": "3.8.1",
|
"prisma": "3.9.1",
|
||||||
"reflect-metadata": "0.1.13",
|
"reflect-metadata": "0.1.13",
|
||||||
"round-to": "5.0.0",
|
"round-to": "5.0.0",
|
||||||
"rxjs": "7.4.0",
|
"rxjs": "7.4.0",
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Order" ALTER COLUMN "dataSource" DROP NOT NULL;
|
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Order" ALTER COLUMN "symbol" DROP NOT NULL;
|
@ -0,0 +1,8 @@
|
|||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "Order" DROP CONSTRAINT "Order_symbolProfileId_fkey";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Order" ALTER COLUMN "symbolProfileId" SET NOT NULL;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Order" ADD CONSTRAINT "Order_symbolProfileId_fkey" FOREIGN KEY ("symbolProfileId") REFERENCES "SymbolProfile"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
@ -0,0 +1,6 @@
|
|||||||
|
-- Set default value
|
||||||
|
UPDATE "User" SET "provider" = 'ANONYMOUS' WHERE "provider" IS NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ALTER COLUMN "provider" SET NOT NULL,
|
||||||
|
ALTER COLUMN "provider" SET DEFAULT E'ANONYMOUS';
|
@ -75,15 +75,15 @@ model Order {
|
|||||||
accountUserId String?
|
accountUserId String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
currency String?
|
currency String?
|
||||||
dataSource DataSource
|
dataSource DataSource?
|
||||||
date DateTime
|
date DateTime
|
||||||
fee Float
|
fee Float
|
||||||
id String @default(uuid())
|
id String @default(uuid())
|
||||||
isDraft Boolean @default(false)
|
isDraft Boolean @default(false)
|
||||||
quantity Float
|
quantity Float
|
||||||
symbol String
|
symbol String?
|
||||||
SymbolProfile SymbolProfile? @relation(fields: [symbolProfileId], references: [id])
|
SymbolProfile SymbolProfile @relation(fields: [symbolProfileId], references: [id])
|
||||||
symbolProfileId String?
|
symbolProfileId String
|
||||||
type Type
|
type Type
|
||||||
unitPrice Float
|
unitPrice Float
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@ -156,7 +156,7 @@ model User {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
Order Order[]
|
Order Order[]
|
||||||
provider Provider?
|
provider Provider @default(ANONYMOUS)
|
||||||
role Role @default(USER)
|
role Role @default(USER)
|
||||||
Settings Settings?
|
Settings Settings?
|
||||||
Subscription Subscription[]
|
Subscription Subscription[]
|
||||||
|
@ -78,30 +78,6 @@ async function main() {
|
|||||||
where: { id: '1377d9df-0d25-42c2-9d9b-e4c63156291f' }
|
where: { id: '1377d9df-0d25-42c2-9d9b-e4c63156291f' }
|
||||||
});
|
});
|
||||||
|
|
||||||
const userAdmin = await prisma.user.upsert({
|
|
||||||
create: {
|
|
||||||
accessToken:
|
|
||||||
'c689bcc894e4a420cb609ee34271f3e07f200594f7d199c50d75add7102889eb60061a04cd2792ebc853c54e37308271271e7bf588657c9e0c37faacbc28c3c6',
|
|
||||||
Account: {
|
|
||||||
create: [
|
|
||||||
{
|
|
||||||
accountType: AccountType.SECURITIES,
|
|
||||||
balance: 0,
|
|
||||||
currency: 'USD',
|
|
||||||
id: 'f4425b66-9ba9-4ac4-93d7-fdf9a145e8cb',
|
|
||||||
isDefault: true,
|
|
||||||
name: 'Default Account'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
alias: 'Admin',
|
|
||||||
id: '4e1af723-95f6-44f8-92a7-464df17f6ec3',
|
|
||||||
role: Role.ADMIN
|
|
||||||
},
|
|
||||||
update: {},
|
|
||||||
where: { id: '4e1af723-95f6-44f8-92a7-464df17f6ec3' }
|
|
||||||
});
|
|
||||||
|
|
||||||
const userDemo = await prisma.user.upsert({
|
const userDemo = await prisma.user.upsert({
|
||||||
create: {
|
create: {
|
||||||
accessToken:
|
accessToken:
|
||||||
@ -345,7 +321,6 @@ async function main() {
|
|||||||
platformInteractiveBrokers,
|
platformInteractiveBrokers,
|
||||||
platformPostFinance,
|
platformPostFinance,
|
||||||
platformSwissquote,
|
platformSwissquote,
|
||||||
userAdmin,
|
|
||||||
userDemo
|
userDemo
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
36
yarn.lock
36
yarn.lock
@ -3349,22 +3349,22 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.10.1.tgz#728ecd95ab207aab8a9a4e421f0422db329232be"
|
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.10.1.tgz#728ecd95ab207aab8a9a4e421f0422db329232be"
|
||||||
integrity sha512-HnUhk1Sy9IuKrxEMdIRCxpIqPw6BFsbYSEUO9p/hNw5sMld/+3OLMWQP80F8/db9qsv3qUjs7ZR5bS/R+iinXw==
|
integrity sha512-HnUhk1Sy9IuKrxEMdIRCxpIqPw6BFsbYSEUO9p/hNw5sMld/+3OLMWQP80F8/db9qsv3qUjs7ZR5bS/R+iinXw==
|
||||||
|
|
||||||
"@prisma/client@3.8.1":
|
"@prisma/client@3.9.1":
|
||||||
version "3.8.1"
|
version "3.9.1"
|
||||||
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.8.1.tgz#c11eda8e84760867552ffde4de7b48fb2cf1e1c0"
|
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.9.1.tgz#565c8121f1220637bcab4a1d1f106b8c1334406c"
|
||||||
integrity sha512-NxD1Xbkx1eT1mxSwo1RwZe665mqBETs0VxohuwNfFIxMqcp0g6d4TgugPxwZ4Jb4e5wCu8mQ9quMedhNWIWcZQ==
|
integrity sha512-aLwfXKLvL+loQ0IuPPCXkcq8cXBg1IeoHHa5lqQu3dJHdj45wnislA/Ny4UxRQjD5FXqrfAb8sWtF+jhdmjFTg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@prisma/engines-version" "3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f"
|
"@prisma/engines-version" "3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009"
|
||||||
|
|
||||||
"@prisma/engines-version@3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f":
|
"@prisma/engines-version@3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009":
|
||||||
version "3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f"
|
version "3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009"
|
||||||
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f.tgz#4c8d9744b5e54650a8ba5fde0a711399d6adba24"
|
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009.tgz#ea03ffa723382a526dc6625ce6eae9b6ad984400"
|
||||||
integrity sha512-G2JH6yWt6ixGKmsRmVgaQYahfwMopim0u/XLIZUo2o/mZ5jdu7+BL+2V5lZr7XiG1axhyrpvlyqE/c0OgYSl3g==
|
integrity sha512-5Dh+qTDhpPR66w6NNAnPs+/W/Qt4r1DSd+qhfPFcDThUK4uxoZKGlPb2IYQn5LL+18aIGnmteDf7BnVMmvBNSQ==
|
||||||
|
|
||||||
"@prisma/engines@3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f":
|
"@prisma/engines@3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009":
|
||||||
version "3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f"
|
version "3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009"
|
||||||
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f.tgz#4479099b99f6a082ce5843ee7208943ccedd127f"
|
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009.tgz#e5c345cdedb7be83d11c1e0c5ab61d866b411256"
|
||||||
integrity sha512-bHYubuItSN/DGYo36aDu7xJiJmK52JOSHs4MK+KbceAtwS20BCWadRgtpQ3iZ2EXfN/B1T0iCXlNraaNwnpU2w==
|
integrity sha512-qM+uJbkelB21bnK44gYE049YTHIjHysOuj0mj5U2gDGyNLfmiazlggzFPCgEjgme4U5YB2tYs6Z5Hq08Kl8pjA==
|
||||||
|
|
||||||
"@samverschueren/stream-to-observable@^0.3.0":
|
"@samverschueren/stream-to-observable@^0.3.0":
|
||||||
version "0.3.1"
|
version "0.3.1"
|
||||||
@ -15025,12 +15025,12 @@ pretty-hrtime@^1.0.3:
|
|||||||
resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
|
resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
|
||||||
integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=
|
integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=
|
||||||
|
|
||||||
prisma@3.8.1:
|
prisma@3.9.1:
|
||||||
version "3.8.1"
|
version "3.9.1"
|
||||||
resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.8.1.tgz#44395cef7cbb1ea86216cb84ee02f856c08a7873"
|
resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.9.1.tgz#7510a8bf06018a5313b9427b1127ce4750b1ce5c"
|
||||||
integrity sha512-Q8zHwS9m70TaD7qI8u+8hTAmiTpK+IpvRYF3Rgb/OeWGQJOMgZCFFvNCiSfoLEQ95wilK7ctW3KOpc9AuYnRUA==
|
integrity sha512-IGcJAu5LzlFv+i+NNhOEh1J1xVVttsVdRBxmrMN7eIH+7mRN6L89Hz1npUAiz4jOpNlHC7n9QwaOYZGxTqlwQw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@prisma/engines" "3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f"
|
"@prisma/engines" "3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009"
|
||||||
|
|
||||||
prismjs@^1.21.0, prismjs@~1.24.0:
|
prismjs@^1.21.0, prismjs@~1.24.0:
|
||||||
version "1.24.1"
|
version "1.24.1"
|
||||||
|
Reference in New Issue
Block a user