Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
5422df05b3 | |||
d2fabe7ce4 | |||
a42700b9fe | |||
9df8541145 | |||
0b2252755c | |||
239bd09cbd | |||
cd76f89902 | |||
7425ba94f1 | |||
b9522307c4 |
25
CHANGELOG.md
25
CHANGELOG.md
@ -5,6 +5,31 @@ 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.75.0 - 13.11.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added a logo to the log on the server start
|
||||||
|
- Added the data gathering progress to the log and the admin control panel
|
||||||
|
- Added the value column to the accounts table
|
||||||
|
|
||||||
|
## 1.74.0 - 11.11.2021
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Adapted the decimal places for cryptocurrencies in the position detail dialog
|
||||||
|
- Moved the _Fear & Greed Index_ (market mood) to a new tab on the home page
|
||||||
|
|
||||||
|
## 1.73.0 - 10.11.2021
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the info messages to add the first transaction
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the skeleton loader of the portfolio holdings
|
||||||
|
|
||||||
## 1.72.0 - 08.11.2021
|
## 1.72.0 - 08.11.2021
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
|
||||||
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||||
import { nullifyValuesInObjects } from '@ghostfolio/api/helper/object.helper';
|
import { nullifyValuesInObjects } from '@ghostfolio/api/helper/object.helper';
|
||||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
||||||
@ -6,7 +7,10 @@ import {
|
|||||||
hasPermission,
|
hasPermission,
|
||||||
permissions
|
permissions
|
||||||
} from '@ghostfolio/common/permissions';
|
} from '@ghostfolio/common/permissions';
|
||||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
import type {
|
||||||
|
AccountWithValue,
|
||||||
|
RequestWithUser
|
||||||
|
} from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
@ -34,6 +38,7 @@ export class AccountController {
|
|||||||
public constructor(
|
public constructor(
|
||||||
private readonly accountService: AccountService,
|
private readonly accountService: AccountService,
|
||||||
private readonly impersonationService: ImpersonationService,
|
private readonly impersonationService: ImpersonationService,
|
||||||
|
private readonly portfolioService: PortfolioService,
|
||||||
@Inject(REQUEST) private readonly request: RequestWithUser,
|
@Inject(REQUEST) private readonly request: RequestWithUser,
|
||||||
private readonly userService: UserService
|
private readonly userService: UserService
|
||||||
) {}
|
) {}
|
||||||
@ -85,14 +90,14 @@ export class AccountController {
|
|||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
public async getAllAccounts(
|
public async getAllAccounts(
|
||||||
@Headers('impersonation-id') impersonationId
|
@Headers('impersonation-id') impersonationId
|
||||||
): Promise<AccountModel[]> {
|
): Promise<AccountWithValue[]> {
|
||||||
const impersonationUserId =
|
const impersonationUserId =
|
||||||
await this.impersonationService.validateImpersonationId(
|
await this.impersonationService.validateImpersonationId(
|
||||||
impersonationId,
|
impersonationId,
|
||||||
this.request.user.id
|
this.request.user.id
|
||||||
);
|
);
|
||||||
|
|
||||||
let accounts = await this.accountService.getAccounts(
|
let accounts = await this.portfolioService.getAccounts(
|
||||||
impersonationUserId || this.request.user.id
|
impersonationUserId || this.request.user.id
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -102,9 +107,11 @@ export class AccountController {
|
|||||||
) {
|
) {
|
||||||
accounts = nullifyValuesInObjects(accounts, [
|
accounts = nullifyValuesInObjects(accounts, [
|
||||||
'balance',
|
'balance',
|
||||||
|
'convertedBalance',
|
||||||
'fee',
|
'fee',
|
||||||
'quantity',
|
'quantity',
|
||||||
'unitPrice'
|
'unitPrice',
|
||||||
|
'value'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module';
|
||||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||||
@ -11,16 +12,17 @@ import { AccountController } from './account.controller';
|
|||||||
import { AccountService } from './account.service';
|
import { AccountService } from './account.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
controllers: [AccountController],
|
||||||
imports: [
|
imports: [
|
||||||
ConfigurationModule,
|
ConfigurationModule,
|
||||||
DataProviderModule,
|
DataProviderModule,
|
||||||
ExchangeRateDataModule,
|
ExchangeRateDataModule,
|
||||||
ImpersonationModule,
|
ImpersonationModule,
|
||||||
RedisCacheModule,
|
PortfolioModule,
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
|
RedisCacheModule,
|
||||||
UserModule
|
UserModule
|
||||||
],
|
],
|
||||||
controllers: [AccountController],
|
|
||||||
providers: [AccountService]
|
providers: [AccountService]
|
||||||
})
|
})
|
||||||
export class AccountModule {}
|
export class AccountModule {}
|
||||||
|
@ -20,6 +20,8 @@ export class AdminService {
|
|||||||
|
|
||||||
public async get(): Promise<AdminData> {
|
public async get(): Promise<AdminData> {
|
||||||
return {
|
return {
|
||||||
|
dataGatheringProgress:
|
||||||
|
await this.dataGatheringService.getDataGatheringProgress(),
|
||||||
exchangeRates: this.exchangeRateDataService
|
exchangeRates: this.exchangeRateDataService
|
||||||
.getCurrencies()
|
.getCurrencies()
|
||||||
.filter((currency) => {
|
.filter((currency) => {
|
||||||
@ -58,7 +60,7 @@ export class AdminService {
|
|||||||
return 'IN_PROGRESS';
|
return 'IN_PROGRESS';
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getUsersWithAnalytics(): Promise<AdminData['users']> {
|
private async getUsersWithAnalytics(): Promise<AdminData['users']> {
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
|
import { AssetClass, AssetSubClass } from '@prisma/client';
|
||||||
|
|
||||||
export interface PortfolioPositionDetail {
|
export interface PortfolioPositionDetail {
|
||||||
|
assetClass?: AssetClass;
|
||||||
|
assetSubClass?: AssetSubClass;
|
||||||
averagePrice: number;
|
averagePrice: number;
|
||||||
currency: string;
|
currency: string;
|
||||||
firstBuyDate: string;
|
firstBuyDate: string;
|
||||||
|
@ -18,6 +18,7 @@ import { PortfolioService } from './portfolio.service';
|
|||||||
import { RulesService } from './rules.service';
|
import { RulesService } from './rules.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
exports: [PortfolioService],
|
||||||
imports: [
|
imports: [
|
||||||
AccessModule,
|
AccessModule,
|
||||||
ConfigurationModule,
|
ConfigurationModule,
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
// TODO ///////////
|
|
||||||
|
|
||||||
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||||
import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details.interface';
|
import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details.interface';
|
||||||
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
||||||
@ -39,6 +37,7 @@ import {
|
|||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||||
import type {
|
import type {
|
||||||
|
AccountWithValue,
|
||||||
DateRange,
|
DateRange,
|
||||||
OrderWithAccount,
|
OrderWithAccount,
|
||||||
RequestWithUser
|
RequestWithUser
|
||||||
@ -81,6 +80,36 @@ export class PortfolioService {
|
|||||||
private readonly symbolProfileService: SymbolProfileService
|
private readonly symbolProfileService: SymbolProfileService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
public async getAccounts(aUserId: string): Promise<AccountWithValue[]> {
|
||||||
|
const [accounts, details] = await Promise.all([
|
||||||
|
this.accountService.accounts({
|
||||||
|
include: { Order: true, Platform: true },
|
||||||
|
orderBy: { name: 'asc' },
|
||||||
|
where: { userId: aUserId }
|
||||||
|
}),
|
||||||
|
this.getDetails(aUserId, aUserId)
|
||||||
|
]);
|
||||||
|
|
||||||
|
const userCurrency = this.request.user.Settings.currency;
|
||||||
|
|
||||||
|
return accounts.map((account) => {
|
||||||
|
const result = {
|
||||||
|
...account,
|
||||||
|
convertedBalance: this.exchangeRateDataService.toCurrency(
|
||||||
|
account.balance,
|
||||||
|
account.currency,
|
||||||
|
userCurrency
|
||||||
|
),
|
||||||
|
transactionCount: account.Order.length,
|
||||||
|
value: details.accounts[account.name].current
|
||||||
|
};
|
||||||
|
|
||||||
|
delete result.Order;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public async getInvestments(
|
public async getInvestments(
|
||||||
aImpersonationId: string
|
aImpersonationId: string
|
||||||
): Promise<InvestmentItem[]> {
|
): Promise<InvestmentItem[]> {
|
||||||
@ -258,7 +287,7 @@ export class PortfolioService {
|
|||||||
value: totalValue
|
value: totalValue
|
||||||
});
|
});
|
||||||
|
|
||||||
const accounts = await this.getAccounts(
|
const accounts = await this.getValueOfAccounts(
|
||||||
orders,
|
orders,
|
||||||
portfolioItemsNow,
|
portfolioItemsNow,
|
||||||
userCurrency,
|
userCurrency,
|
||||||
@ -299,6 +328,8 @@ export class PortfolioService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const assetClass = orders[0].SymbolProfile?.assetClass;
|
||||||
|
const assetSubClass = orders[0].SymbolProfile?.assetSubClass;
|
||||||
const positionCurrency = orders[0].currency;
|
const positionCurrency = orders[0].currency;
|
||||||
const name = orders[0].SymbolProfile?.name ?? '';
|
const name = orders[0].SymbolProfile?.name ?? '';
|
||||||
|
|
||||||
@ -412,6 +443,8 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
assetClass,
|
||||||
|
assetSubClass,
|
||||||
currency,
|
currency,
|
||||||
firstBuyDate,
|
firstBuyDate,
|
||||||
grossPerformance,
|
grossPerformance,
|
||||||
@ -467,6 +500,8 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
assetClass,
|
||||||
|
assetSubClass,
|
||||||
marketPrice,
|
marketPrice,
|
||||||
maxPrice,
|
maxPrice,
|
||||||
minPrice,
|
minPrice,
|
||||||
@ -613,7 +648,7 @@ export class PortfolioService {
|
|||||||
currentGrossPerformancePercent,
|
currentGrossPerformancePercent,
|
||||||
currentNetPerformance,
|
currentNetPerformance,
|
||||||
currentNetPerformancePercent,
|
currentNetPerformancePercent,
|
||||||
currentValue: currentValue
|
currentValue
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -663,7 +698,7 @@ export class PortfolioService {
|
|||||||
for (const position of currentPositions.positions) {
|
for (const position of currentPositions.positions) {
|
||||||
portfolioItemsNow[position.symbol] = position;
|
portfolioItemsNow[position.symbol] = position;
|
||||||
}
|
}
|
||||||
const accounts = await this.getAccounts(
|
const accounts = await this.getValueOfAccounts(
|
||||||
orders,
|
orders,
|
||||||
portfolioItemsNow,
|
portfolioItemsNow,
|
||||||
currency,
|
currency,
|
||||||
@ -863,7 +898,7 @@ export class PortfolioService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getAccounts(
|
private async getValueOfAccounts(
|
||||||
orders: OrderWithAccount[],
|
orders: OrderWithAccount[],
|
||||||
portfolioItemsNow: { [p: string]: TimelinePosition },
|
portfolioItemsNow: { [p: string]: TimelinePosition },
|
||||||
userCurrency: string,
|
userCurrency: string,
|
||||||
@ -878,21 +913,16 @@ export class PortfolioService {
|
|||||||
return accountId === account.id;
|
return accountId === account.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (ordersByAccount.length <= 0) {
|
const convertedBalance = this.exchangeRateDataService.toCurrency(
|
||||||
// Add account without orders
|
|
||||||
const balance = this.exchangeRateDataService.toCurrency(
|
|
||||||
account.balance,
|
account.balance,
|
||||||
account.currency,
|
account.currency,
|
||||||
userCurrency
|
userCurrency
|
||||||
);
|
);
|
||||||
accounts[account.name] = {
|
accounts[account.name] = {
|
||||||
current: balance,
|
current: convertedBalance,
|
||||||
original: balance
|
original: convertedBalance
|
||||||
};
|
};
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const order of ordersByAccount) {
|
for (const order of ordersByAccount) {
|
||||||
let currentValueOfSymbol = this.exchangeRateDataService.toCurrency(
|
let currentValueOfSymbol = this.exchangeRateDataService.toCurrency(
|
||||||
order.quantity * portfolioItemsNow[order.symbol].marketPrice,
|
order.quantity * portfolioItemsNow[order.symbol].marketPrice,
|
||||||
|
@ -2,6 +2,7 @@ import { Logger, ValidationPipe } from '@nestjs/common';
|
|||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
|
||||||
import { AppModule } from './app/app.module';
|
import { AppModule } from './app/app.module';
|
||||||
|
import { environment } from './environments/environment';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
@ -18,8 +19,23 @@ async function bootstrap() {
|
|||||||
|
|
||||||
const port = process.env.PORT || 3333;
|
const port = process.env.PORT || 3333;
|
||||||
await app.listen(port, () => {
|
await app.listen(port, () => {
|
||||||
Logger.log(`Listening at http://localhost:${port}`);
|
logLogo();
|
||||||
|
Logger.log(`Listening at http://localhost:${port}`, '', false);
|
||||||
|
Logger.log('', '', false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function logLogo() {
|
||||||
|
Logger.log(' ________ __ ____ ___', '', false);
|
||||||
|
Logger.log(' / ____/ /_ ____ _____/ /_/ __/___ / (_)___', '', false);
|
||||||
|
Logger.log(' / / __/ __ \\/ __ \\/ ___/ __/ /_/ __ \\/ / / __ \\', '', false);
|
||||||
|
Logger.log('/ /_/ / / / / /_/ (__ ) /_/ __/ /_/ / / / /_/ /', '', false);
|
||||||
|
Logger.log(
|
||||||
|
`\\____/_/ /_/\\____/____/\\__/_/ \\____/_/_/\\____/ ${environment.version}`,
|
||||||
|
'',
|
||||||
|
false
|
||||||
|
);
|
||||||
|
Logger.log('', '', false);
|
||||||
|
}
|
||||||
|
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
@ -18,7 +18,6 @@ import {
|
|||||||
|
|
||||||
import { ConfigurationService } from './configuration.service';
|
import { ConfigurationService } from './configuration.service';
|
||||||
import { DataProviderService } from './data-provider/data-provider.service';
|
import { DataProviderService } from './data-provider/data-provider.service';
|
||||||
import { GhostfolioScraperApiService } from './data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
|
||||||
import { DataEnhancerInterface } from './data-provider/interfaces/data-enhancer.interface';
|
import { DataEnhancerInterface } from './data-provider/interfaces/data-enhancer.interface';
|
||||||
import { ExchangeRateDataService } from './exchange-rate-data.service';
|
import { ExchangeRateDataService } from './exchange-rate-data.service';
|
||||||
import { IDataGatheringItem } from './interfaces/interfaces';
|
import { IDataGatheringItem } from './interfaces/interfaces';
|
||||||
@ -26,13 +25,14 @@ import { PrismaService } from './prisma.service';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DataGatheringService {
|
export class DataGatheringService {
|
||||||
|
private dataGatheringProgress: number;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly configurationService: ConfigurationService,
|
private readonly configurationService: ConfigurationService,
|
||||||
@Inject('DataEnhancers')
|
@Inject('DataEnhancers')
|
||||||
private readonly dataEnhancers: DataEnhancerInterface[],
|
private readonly dataEnhancers: DataEnhancerInterface[],
|
||||||
private readonly dataProviderService: DataProviderService,
|
private readonly dataProviderService: DataProviderService,
|
||||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||||
private readonly ghostfolioScraperApi: GhostfolioScraperApiService,
|
|
||||||
private readonly prismaService: PrismaService,
|
private readonly prismaService: PrismaService,
|
||||||
private readonly symbolProfileService: SymbolProfileService
|
private readonly symbolProfileService: SymbolProfileService
|
||||||
) {}
|
) {}
|
||||||
@ -204,8 +204,11 @@ export class DataGatheringService {
|
|||||||
|
|
||||||
public async gatherSymbols(aSymbolsWithStartDate: IDataGatheringItem[]) {
|
public async gatherSymbols(aSymbolsWithStartDate: IDataGatheringItem[]) {
|
||||||
let hasError = false;
|
let hasError = false;
|
||||||
|
let symbolCounter = 0;
|
||||||
|
|
||||||
for (const { dataSource, date, symbol } of aSymbolsWithStartDate) {
|
for (const { dataSource, date, symbol } of aSymbolsWithStartDate) {
|
||||||
|
this.dataGatheringProgress = symbolCounter / aSymbolsWithStartDate.length;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const historicalData = await this.dataProviderService.getHistoricalRaw(
|
const historicalData = await this.dataProviderService.getHistoricalRaw(
|
||||||
[{ dataSource, symbol }],
|
[{ dataSource, symbol }],
|
||||||
@ -263,6 +266,16 @@ export class DataGatheringService {
|
|||||||
hasError = true;
|
hasError = true;
|
||||||
Logger.error(error);
|
Logger.error(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (symbolCounter > 0 && symbolCounter % 100 === 0) {
|
||||||
|
Logger.log(
|
||||||
|
`Data gathering progress: ${(
|
||||||
|
this.dataGatheringProgress * 100
|
||||||
|
).toFixed(2)}%`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
symbolCounter += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.exchangeRateDataService.initialize();
|
await this.exchangeRateDataService.initialize();
|
||||||
@ -272,6 +285,16 @@ export class DataGatheringService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getDataGatheringProgress() {
|
||||||
|
const isInProgress = await this.getIsInProgress();
|
||||||
|
|
||||||
|
if (isInProgress) {
|
||||||
|
return this.dataGatheringProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
public async getIsInProgress() {
|
public async getIsInProgress() {
|
||||||
return await this.prismaService.property.findUnique({
|
return await this.prismaService.property.findUnique({
|
||||||
where: { key: 'LOCKED_DATA_GATHERING' }
|
where: { key: 'LOCKED_DATA_GATHERING' }
|
||||||
|
@ -17,6 +17,20 @@
|
|||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="currency">
|
||||||
|
<th
|
||||||
|
*matHeaderCellDef
|
||||||
|
class="d-none d-lg-table-cell px-1"
|
||||||
|
i18n
|
||||||
|
mat-header-cell
|
||||||
|
>
|
||||||
|
Currency
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
|
||||||
|
{{ element.currency }}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="platform">
|
<ng-container matColumnDef="platform">
|
||||||
<th
|
<th
|
||||||
*matHeaderCellDef
|
*matHeaderCellDef
|
||||||
@ -45,7 +59,9 @@
|
|||||||
<span class="d-none d-sm-block" i18n>Transactions</span>
|
<span class="d-none d-sm-block" i18n>Transactions</span>
|
||||||
</th>
|
</th>
|
||||||
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
||||||
{{ element.transactionCount }}
|
<ng-container *ngIf="element.accountType === 'SECURITIES'">{{
|
||||||
|
element.transactionCount
|
||||||
|
}}</ng-container>
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
@ -56,9 +72,23 @@
|
|||||||
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
||||||
<gf-value
|
<gf-value
|
||||||
class="d-inline-block justify-content-end"
|
class="d-inline-block justify-content-end"
|
||||||
[currency]="element.currency"
|
[isCurrency]="true"
|
||||||
[locale]="locale"
|
[locale]="locale"
|
||||||
[value]="element.balance"
|
[value]="element.convertedBalance"
|
||||||
|
></gf-value>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="value">
|
||||||
|
<th *matHeaderCellDef class="px-1 text-right" i18n mat-header-cell>
|
||||||
|
Value
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
||||||
|
<gf-value
|
||||||
|
class="d-inline-block justify-content-end"
|
||||||
|
[isCurrency]="true"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="element.value"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -41,7 +41,14 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
public ngOnInit() {}
|
public ngOnInit() {}
|
||||||
|
|
||||||
public ngOnChanges() {
|
public ngOnChanges() {
|
||||||
this.displayedColumns = ['account', 'platform', 'transactions', 'balance'];
|
this.displayedColumns = [
|
||||||
|
'account',
|
||||||
|
'currency',
|
||||||
|
'platform',
|
||||||
|
'transactions',
|
||||||
|
'balance',
|
||||||
|
'value'
|
||||||
|
];
|
||||||
|
|
||||||
if (this.showActions) {
|
if (this.showActions) {
|
||||||
this.displayedColumns.push('actions');
|
this.displayedColumns.push('actions');
|
||||||
|
@ -2,6 +2,5 @@ import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.i
|
|||||||
|
|
||||||
export interface PositionDetailDialogParams {
|
export interface PositionDetailDialogParams {
|
||||||
deviceType: string;
|
deviceType: string;
|
||||||
fearAndGreedIndex: number;
|
|
||||||
historicalDataItems: LineChartItem[];
|
historicalDataItems: LineChartItem[];
|
||||||
}
|
}
|
||||||
|
@ -22,13 +22,11 @@ import { PositionDetailDialogParams } from './interfaces/interfaces';
|
|||||||
})
|
})
|
||||||
export class PerformanceChartDialog {
|
export class PerformanceChartDialog {
|
||||||
public benchmarkDataItems: LineChartItem[];
|
public benchmarkDataItems: LineChartItem[];
|
||||||
public benchmarkLabel = 'S&P 500';
|
|
||||||
public benchmarkSymbol = 'VOO';
|
public benchmarkSymbol = 'VOO';
|
||||||
public currency: string;
|
public currency: string;
|
||||||
public firstBuyDate: string;
|
public firstBuyDate: string;
|
||||||
public marketPrice: number;
|
public marketPrice: number;
|
||||||
public historicalDataItems: LineChartItem[];
|
public historicalDataItems: LineChartItem[];
|
||||||
public title: string;
|
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
@ -83,8 +81,6 @@ export class PerformanceChartDialog {
|
|||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.title = `Performance vs. ${this.benchmarkLabel}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public onClose(): void {
|
public onClose(): void {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<gf-dialog-header
|
<gf-dialog-header
|
||||||
mat-dialog-title
|
mat-dialog-title
|
||||||
|
title="Performance"
|
||||||
[deviceType]="data.deviceType"
|
[deviceType]="data.deviceType"
|
||||||
[title]="title"
|
|
||||||
(closeButtonClicked)="onClose()"
|
(closeButtonClicked)="onClose()"
|
||||||
></gf-dialog-header>
|
></gf-dialog-header>
|
||||||
|
|
||||||
@ -11,7 +11,6 @@
|
|||||||
class="mb-4"
|
class="mb-4"
|
||||||
symbol="Performance"
|
symbol="Performance"
|
||||||
[benchmarkDataItems]="benchmarkDataItems"
|
[benchmarkDataItems]="benchmarkDataItems"
|
||||||
[benchmarkLabel]="benchmarkLabel"
|
|
||||||
[historicalDataItems]="historicalDataItems"
|
[historicalDataItems]="historicalDataItems"
|
||||||
[showGradient]="true"
|
[showGradient]="true"
|
||||||
[showLegend]="true"
|
[showLegend]="true"
|
||||||
@ -19,13 +18,6 @@
|
|||||||
[showYAxis]="false"
|
[showYAxis]="false"
|
||||||
></gf-line-chart>
|
></gf-line-chart>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="data.fearAndGreedIndex" class="container p-0">
|
|
||||||
<gf-fear-and-greed-index
|
|
||||||
class="d-flex flex-column justify-content-center"
|
|
||||||
[fearAndGreedIndex]="data.fearAndGreedIndex"
|
|
||||||
></gf-fear-and-greed-index>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<gf-dialog-footer
|
<gf-dialog-footer
|
||||||
|
@ -8,7 +8,6 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
|||||||
|
|
||||||
import { GfDialogFooterModule } from '../dialog-footer/dialog-footer.module';
|
import { GfDialogFooterModule } from '../dialog-footer/dialog-footer.module';
|
||||||
import { GfDialogHeaderModule } from '../dialog-header/dialog-header.module';
|
import { GfDialogHeaderModule } from '../dialog-header/dialog-header.module';
|
||||||
import { GfFearAndGreedIndexModule } from '../fear-and-greed-index/fear-and-greed-index.module';
|
|
||||||
import { PerformanceChartDialog } from './performance-chart-dialog.component';
|
import { PerformanceChartDialog } from './performance-chart-dialog.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@ -18,7 +17,6 @@ import { PerformanceChartDialog } from './performance-chart-dialog.component';
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
GfDialogFooterModule,
|
GfDialogFooterModule,
|
||||||
GfDialogHeaderModule,
|
GfDialogHeaderModule,
|
||||||
GfFearAndGreedIndexModule,
|
|
||||||
GfLineChartModule,
|
GfLineChartModule,
|
||||||
GfValueModule,
|
GfValueModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
|
@ -9,6 +9,7 @@ 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 } from '@ghostfolio/common/helper';
|
||||||
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 { format, isSameMonth, isToday, parseISO } from 'date-fns';
|
import { format, isSameMonth, isToday, parseISO } from 'date-fns';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
@ -23,6 +24,7 @@ import { PositionDetailDialogParams } from './interfaces/interfaces';
|
|||||||
styleUrls: ['./position-detail-dialog.component.scss']
|
styleUrls: ['./position-detail-dialog.component.scss']
|
||||||
})
|
})
|
||||||
export class PositionDetailDialog implements OnDestroy {
|
export class PositionDetailDialog implements OnDestroy {
|
||||||
|
public assetSubClass: AssetSubClass;
|
||||||
public averagePrice: number;
|
public averagePrice: number;
|
||||||
public benchmarkDataItems: LineChartItem[];
|
public benchmarkDataItems: LineChartItem[];
|
||||||
public currency: string;
|
public currency: string;
|
||||||
@ -38,6 +40,7 @@ export class PositionDetailDialog implements OnDestroy {
|
|||||||
public netPerformance: number;
|
public netPerformance: number;
|
||||||
public netPerformancePercent: number;
|
public netPerformancePercent: number;
|
||||||
public quantity: number;
|
public quantity: number;
|
||||||
|
public quantityPrecision = 2;
|
||||||
public symbol: string;
|
public symbol: string;
|
||||||
public transactionCount: number;
|
public transactionCount: number;
|
||||||
|
|
||||||
@ -54,6 +57,7 @@ export class PositionDetailDialog implements OnDestroy {
|
|||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(
|
.subscribe(
|
||||||
({
|
({
|
||||||
|
assetSubClass,
|
||||||
averagePrice,
|
averagePrice,
|
||||||
currency,
|
currency,
|
||||||
firstBuyDate,
|
firstBuyDate,
|
||||||
@ -71,6 +75,7 @@ export class PositionDetailDialog implements OnDestroy {
|
|||||||
symbol,
|
symbol,
|
||||||
transactionCount
|
transactionCount
|
||||||
}) => {
|
}) => {
|
||||||
|
this.assetSubClass = assetSubClass;
|
||||||
this.averagePrice = averagePrice;
|
this.averagePrice = averagePrice;
|
||||||
this.benchmarkDataItems = [];
|
this.benchmarkDataItems = [];
|
||||||
this.currency = currency;
|
this.currency = currency;
|
||||||
@ -146,6 +151,18 @@ export class PositionDetailDialog implements OnDestroy {
|
|||||||
this.benchmarkDataItems[0].value = this.averagePrice;
|
this.benchmarkDataItems[0].value = this.averagePrice;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Number.isInteger(this.quantity)) {
|
||||||
|
this.quantityPrecision = 0;
|
||||||
|
} else if (assetSubClass === 'CRYPTOCURRENCY') {
|
||||||
|
if (this.quantity < 1) {
|
||||||
|
this.quantityPrecision = 7;
|
||||||
|
} else if (this.quantity < 1000) {
|
||||||
|
this.quantityPrecision = 5;
|
||||||
|
} else if (this.quantity > 10000000) {
|
||||||
|
this.quantityPrecision = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -82,7 +82,7 @@
|
|||||||
label="Quantity"
|
label="Quantity"
|
||||||
size="medium"
|
size="medium"
|
||||||
[locale]="data.locale"
|
[locale]="data.locale"
|
||||||
[precision]="2"
|
[precision]="quantityPrecision"
|
||||||
[value]="quantity"
|
[value]="quantity"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
|
@ -103,7 +103,9 @@
|
|||||||
></ngx-skeleton-loader>
|
></ngx-skeleton-loader>
|
||||||
|
|
||||||
<div *ngIf="dataSource.data.length === 0 && !isLoading" class="p-3 text-center">
|
<div *ngIf="dataSource.data.length === 0 && !isLoading" class="p-3 text-center">
|
||||||
<gf-no-transactions-info-indicator></gf-no-transactions-info-indicator>
|
<gf-no-transactions-info-indicator
|
||||||
|
[hasBorder]="false"
|
||||||
|
></gf-no-transactions-info-indicator>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -24,7 +24,9 @@
|
|||||||
></gf-position>
|
></gf-position>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<div *ngIf="!hasPositions" class="p-3 text-center">
|
<div *ngIf="!hasPositions" class="p-3 text-center">
|
||||||
<gf-no-transactions-info-indicator></gf-no-transactions-info-indicator>
|
<gf-no-transactions-info-indicator
|
||||||
|
[hasBorder]="false"
|
||||||
|
></gf-no-transactions-info-indicator>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,7 +2,9 @@
|
|||||||
<div class="row no-gutters">
|
<div class="row no-gutters">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<mat-card *ngIf="rules === null" class="my-2 text-center">
|
<mat-card *ngIf="rules === null" class="my-2 text-center">
|
||||||
<gf-no-transactions-info-indicator></gf-no-transactions-info-indicator>
|
<gf-no-transactions-info-indicator
|
||||||
|
[hasBorder]="false"
|
||||||
|
></gf-no-transactions-info-indicator>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
|
||||||
<gf-rule *ngIf="rules === undefined" [isLoading]="true"></gf-rule>
|
<gf-rule *ngIf="rules === undefined" [isLoading]="true"></gf-rule>
|
||||||
|
@ -270,3 +270,9 @@
|
|||||||
width: '100%'
|
width: '100%'
|
||||||
}"
|
}"
|
||||||
></ngx-skeleton-loader>
|
></ngx-skeleton-loader>
|
||||||
|
|
||||||
|
<div *ngIf="dataSource.data.length === 0 && !isLoading" class="p-3 text-center">
|
||||||
|
<gf-no-transactions-info-indicator
|
||||||
|
[hasBorder]="false"
|
||||||
|
></gf-no-transactions-info-indicator>
|
||||||
|
</div>
|
||||||
|
@ -10,6 +10,7 @@ import { MatSortModule } from '@angular/material/sort';
|
|||||||
import { MatTableModule } from '@angular/material/table';
|
import { MatTableModule } from '@angular/material/table';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||||
|
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
|
||||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
@ -22,6 +23,7 @@ import { TransactionsTableComponent } from './transactions-table.component';
|
|||||||
exports: [TransactionsTableComponent],
|
exports: [TransactionsTableComponent],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
GfNoTransactionsInfoModule,
|
||||||
GfPositionDetailDialogModule,
|
GfPositionDetailDialogModule,
|
||||||
GfSymbolIconModule,
|
GfSymbolIconModule,
|
||||||
GfSymbolModule,
|
GfSymbolModule,
|
||||||
|
@ -22,6 +22,7 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
})
|
})
|
||||||
export class AdminPageComponent implements OnDestroy, OnInit {
|
export class AdminPageComponent implements OnDestroy, OnInit {
|
||||||
public dataGatheringInProgress: boolean;
|
public dataGatheringInProgress: boolean;
|
||||||
|
public dataGatheringProgress: number;
|
||||||
public defaultDateFormat = DEFAULT_DATE_FORMAT;
|
public defaultDateFormat = DEFAULT_DATE_FORMAT;
|
||||||
public exchangeRates: { label1: string; label2: string; value: number }[];
|
public exchangeRates: { label1: string; label2: string; value: number }[];
|
||||||
public lastDataGathering: string;
|
public lastDataGathering: string;
|
||||||
@ -134,12 +135,14 @@ export class AdminPageComponent implements OnDestroy, OnInit {
|
|||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(
|
.subscribe(
|
||||||
({
|
({
|
||||||
|
dataGatheringProgress,
|
||||||
exchangeRates,
|
exchangeRates,
|
||||||
lastDataGathering,
|
lastDataGathering,
|
||||||
transactionCount,
|
transactionCount,
|
||||||
userCount,
|
userCount,
|
||||||
users
|
users
|
||||||
}) => {
|
}) => {
|
||||||
|
this.dataGatheringProgress = dataGatheringProgress;
|
||||||
this.exchangeRates = exchangeRates;
|
this.exchangeRates = exchangeRates;
|
||||||
this.users = users;
|
this.users = users;
|
||||||
|
|
||||||
|
@ -35,14 +35,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex my-3">
|
<div class="d-flex my-3">
|
||||||
<div class="w-50" i18n>Last Data Gathering</div>
|
<div class="w-50" i18n>Data Gathering</div>
|
||||||
<div class="w-50">
|
<div class="w-50">
|
||||||
<div>
|
<div>
|
||||||
<ng-container *ngIf="lastDataGathering"
|
<ng-container *ngIf="lastDataGathering"
|
||||||
>{{ lastDataGathering }}</ng-container
|
>{{ lastDataGathering }}</ng-container
|
||||||
>
|
>
|
||||||
<ng-container *ngIf="dataGatheringInProgress" i18n
|
<ng-container *ngIf="dataGatheringInProgress" i18n
|
||||||
>In Progress</ng-container
|
>In Progress ({{ dataGatheringProgress | percent : '1.2-2'
|
||||||
|
}})</ng-container
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 overflow-hidden">
|
<div class="mt-2 overflow-hidden">
|
||||||
|
@ -7,10 +7,7 @@ import {
|
|||||||
OnInit,
|
OnInit,
|
||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
|
||||||
import { MatTabChangeEvent } from '@angular/material/tabs';
|
import { MatTabChangeEvent } from '@angular/material/tabs';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
|
||||||
import { PerformanceChartDialog } from '@ghostfolio/client/components/performance-chart-dialog/performance-chart-dialog.component';
|
|
||||||
import { ToggleOption } from '@ghostfolio/client/components/toggle/interfaces/toggle-option.type';
|
import { ToggleOption } from '@ghostfolio/client/components/toggle/interfaces/toggle-option.type';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||||
@ -61,7 +58,6 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
|||||||
public hasImpersonationId: boolean;
|
public hasImpersonationId: boolean;
|
||||||
public hasPermissionToAccessFearAndGreedIndex: boolean;
|
public hasPermissionToAccessFearAndGreedIndex: boolean;
|
||||||
public hasPermissionToCreateOrder: boolean;
|
public hasPermissionToCreateOrder: boolean;
|
||||||
public hasPositions: boolean;
|
|
||||||
public historicalDataItems: LineChartItem[];
|
public historicalDataItems: LineChartItem[];
|
||||||
public isLoadingPerformance = true;
|
public isLoadingPerformance = true;
|
||||||
public isLoadingSummary = true;
|
public isLoadingSummary = true;
|
||||||
@ -80,21 +76,10 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
|||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
private deviceService: DeviceDetectorService,
|
private deviceService: DeviceDetectorService,
|
||||||
private dialog: MatDialog,
|
|
||||||
private impersonationStorageService: ImpersonationStorageService,
|
private impersonationStorageService: ImpersonationStorageService,
|
||||||
private route: ActivatedRoute,
|
|
||||||
private router: Router,
|
|
||||||
private settingsStorageService: SettingsStorageService,
|
private settingsStorageService: SettingsStorageService,
|
||||||
private userService: UserService
|
private userService: UserService
|
||||||
) {
|
) {
|
||||||
this.routeQueryParams = this.route.queryParams
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe((params) => {
|
|
||||||
if (params['performanceChartDialog']) {
|
|
||||||
this.openDialog();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.userService.stateChanged
|
this.userService.stateChanged
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((state) => {
|
.subscribe((state) => {
|
||||||
@ -173,25 +158,6 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
|||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
private openDialog(): void {
|
|
||||||
const dialogRef = this.dialog.open(PerformanceChartDialog, {
|
|
||||||
autoFocus: false,
|
|
||||||
data: {
|
|
||||||
deviceType: this.deviceType,
|
|
||||||
fearAndGreedIndex: this.fearAndGreedIndex,
|
|
||||||
historicalDataItems: this.historicalDataItems
|
|
||||||
},
|
|
||||||
width: '50rem'
|
|
||||||
});
|
|
||||||
|
|
||||||
dialogRef
|
|
||||||
.afterClosed()
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe(() => {
|
|
||||||
this.router.navigate(['.'], { relativeTo: this.route });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private update() {
|
private update() {
|
||||||
if (this.currentTabIndex === 0) {
|
if (this.currentTabIndex === 0) {
|
||||||
this.isLoadingPerformance = true;
|
this.isLoadingPerformance = true;
|
||||||
@ -225,7 +191,6 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
|||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((response) => {
|
.subscribe((response) => {
|
||||||
this.positions = response.positions;
|
this.positions = response.positions;
|
||||||
this.hasPositions = this.positions?.length > 0;
|
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
@ -23,12 +23,7 @@
|
|||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div class="row w-100">
|
<div class="row w-100">
|
||||||
<a
|
<div class="chart-container col">
|
||||||
*ngIf="historicalDataItems?.length !== 0"
|
|
||||||
class="chart-container col"
|
|
||||||
[routerLink]="[]"
|
|
||||||
[queryParams]="{performanceChartDialog: true}"
|
|
||||||
>
|
|
||||||
<gf-line-chart
|
<gf-line-chart
|
||||||
class="mr-3"
|
class="mr-3"
|
||||||
symbol="Performance"
|
symbol="Performance"
|
||||||
@ -38,7 +33,6 @@
|
|||||||
[showXAxis]="false"
|
[showXAxis]="false"
|
||||||
[showYAxis]="false"
|
[showYAxis]="false"
|
||||||
></gf-line-chart>
|
></gf-line-chart>
|
||||||
</a>
|
|
||||||
<div
|
<div
|
||||||
*ngIf="historicalDataItems?.length === 0"
|
*ngIf="historicalDataItems?.length === 0"
|
||||||
class="
|
class="
|
||||||
@ -54,6 +48,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="overview-container row mt-1">
|
<div class="overview-container row mt-1">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<gf-portfolio-performance
|
<gf-portfolio-performance
|
||||||
@ -92,7 +87,6 @@
|
|||||||
(change)="onChangeDateRange($event.value)"
|
(change)="onChangeDateRange($event.value)"
|
||||||
></gf-toggle>
|
></gf-toggle>
|
||||||
</div>
|
</div>
|
||||||
<ng-container *ngIf="hasPositions === true">
|
|
||||||
<mat-card class="p-0">
|
<mat-card class="p-0">
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<gf-positions
|
<gf-positions
|
||||||
@ -113,13 +107,6 @@
|
|||||||
>Manage Transactions...</a
|
>Manage Transactions...</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
|
||||||
<div
|
|
||||||
*ngIf="hasPositions === false"
|
|
||||||
class="d-flex justify-content-center"
|
|
||||||
>
|
|
||||||
<gf-no-transactions-info-indicator></gf-no-transactions-info-indicator>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -148,4 +135,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</mat-tab>
|
</mat-tab>
|
||||||
|
<mat-tab *ngIf="hasPermissionToAccessFearAndGreedIndex">
|
||||||
|
<ng-template mat-tab-label>
|
||||||
|
<ion-icon name="newspaper-outline" size="large"></ion-icon>
|
||||||
|
</ng-template>
|
||||||
|
<div
|
||||||
|
class="
|
||||||
|
align-items-center
|
||||||
|
container
|
||||||
|
d-flex
|
||||||
|
flex-grow-1
|
||||||
|
h-100
|
||||||
|
justify-content-center
|
||||||
|
w-100
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="row w-100">
|
||||||
|
<div class="col-xs-12 col-md-8 offset-md-2">
|
||||||
|
<mat-card class="h-100">
|
||||||
|
<mat-card-content>
|
||||||
|
<gf-fear-and-greed-index
|
||||||
|
class="d-flex justify-content-center"
|
||||||
|
[fearAndGreedIndex]="fearAndGreedIndex"
|
||||||
|
></gf-fear-and-greed-index>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-tab>
|
||||||
</mat-tab-group>
|
</mat-tab-group>
|
||||||
|
@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
|
|||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { MatTabsModule } from '@angular/material/tabs';
|
import { MatTabsModule } from '@angular/material/tabs';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { GfFearAndGreedIndexModule } from '@ghostfolio/client/components/fear-and-greed-index/fear-and-greed-index.module';
|
||||||
import { GfPerformanceChartDialogModule } from '@ghostfolio/client/components/performance-chart-dialog/performance-chart-dialog.module';
|
import { GfPerformanceChartDialogModule } from '@ghostfolio/client/components/performance-chart-dialog/performance-chart-dialog.module';
|
||||||
import { GfPortfolioPerformanceModule } from '@ghostfolio/client/components/portfolio-performance/portfolio-performance.module';
|
import { GfPortfolioPerformanceModule } from '@ghostfolio/client/components/portfolio-performance/portfolio-performance.module';
|
||||||
import { GfPortfolioSummaryModule } from '@ghostfolio/client/components/portfolio-summary/portfolio-summary.module';
|
import { GfPortfolioSummaryModule } from '@ghostfolio/client/components/portfolio-summary/portfolio-summary.module';
|
||||||
@ -20,6 +21,7 @@ import { HomePageComponent } from './home-page.component';
|
|||||||
exports: [],
|
exports: [],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
GfFearAndGreedIndexModule,
|
||||||
GfLineChartModule,
|
GfLineChartModule,
|
||||||
GfNoTransactionsInfoModule,
|
GfNoTransactionsInfoModule,
|
||||||
GfPerformanceChartDialogModule,
|
GfPerformanceChartDialogModule,
|
||||||
|
@ -38,7 +38,6 @@ export class ZenPageComponent implements AfterViewInit, OnDestroy, OnInit {
|
|||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
public hasImpersonationId: boolean;
|
public hasImpersonationId: boolean;
|
||||||
public hasPermissionToCreateOrder: boolean;
|
public hasPermissionToCreateOrder: boolean;
|
||||||
public hasPositions: boolean;
|
|
||||||
public historicalDataItems: LineChartItem[];
|
public historicalDataItems: LineChartItem[];
|
||||||
public isLoadingPerformance = true;
|
public isLoadingPerformance = true;
|
||||||
public performance: PortfolioPerformance;
|
public performance: PortfolioPerformance;
|
||||||
@ -140,7 +139,6 @@ export class ZenPageComponent implements AfterViewInit, OnDestroy, OnInit {
|
|||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((response) => {
|
.subscribe((response) => {
|
||||||
this.positions = response.positions;
|
this.positions = response.positions;
|
||||||
this.hasPositions = this.positions?.length > 0;
|
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
@ -65,7 +65,6 @@
|
|||||||
<h3 class="d-flex justify-content-center mb-3" i18n>Holdings</h3>
|
<h3 class="d-flex justify-content-center mb-3" i18n>Holdings</h3>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="align-items-center col">
|
<div class="align-items-center col">
|
||||||
<ng-container *ngIf="hasPositions === true">
|
|
||||||
<mat-card class="p-0">
|
<mat-card class="p-0">
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<gf-positions
|
<gf-positions
|
||||||
@ -86,15 +85,6 @@
|
|||||||
>Manage Transactions...</a
|
>Manage Transactions...</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
|
||||||
<div
|
|
||||||
*ngIf="hasPositions === false"
|
|
||||||
class="d-flex justify-content-center"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<gf-no-transactions-info-indicator></gf-no-transactions-info-indicator>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -29,7 +29,7 @@ import {
|
|||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||||
import { permissions } from '@ghostfolio/common/permissions';
|
import { permissions } from '@ghostfolio/common/permissions';
|
||||||
import { DateRange } from '@ghostfolio/common/types';
|
import { AccountWithValue, DateRange } from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
Account as AccountModel,
|
Account as AccountModel,
|
||||||
DataSource,
|
DataSource,
|
||||||
@ -62,7 +62,7 @@ export class DataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public fetchAccounts() {
|
public fetchAccounts() {
|
||||||
return this.http.get<AccountModel[]>('/api/account');
|
return this.http.get<AccountWithValue[]>('/api/account');
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchAdminData() {
|
public fetchAdminData() {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
export interface AdminData {
|
export interface AdminData {
|
||||||
|
dataGatheringProgress?: number;
|
||||||
exchangeRates: { label1: string; label2: string; value: number }[];
|
exchangeRates: { label1: string; label2: string; value: number }[];
|
||||||
lastDataGathering: Date | 'IN_PROGRESS';
|
lastDataGathering?: Date | 'IN_PROGRESS';
|
||||||
transactionCount: number;
|
transactionCount: number;
|
||||||
userCount: number;
|
userCount: number;
|
||||||
users: {
|
users: {
|
||||||
|
6
libs/common/src/lib/types/account-with-value.type.ts
Normal file
6
libs/common/src/lib/types/account-with-value.type.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { Account as AccountModel } from '@prisma/client';
|
||||||
|
|
||||||
|
export type AccountWithValue = AccountModel & {
|
||||||
|
convertedBalance: number;
|
||||||
|
value: number;
|
||||||
|
};
|
@ -1,4 +1,5 @@
|
|||||||
import type { AccessWithGranteeUser } from './access-with-grantee-user.type';
|
import type { AccessWithGranteeUser } from './access-with-grantee-user.type';
|
||||||
|
import { AccountWithValue } from './account-with-value.type';
|
||||||
import type { DateRange } from './date-range.type';
|
import type { DateRange } from './date-range.type';
|
||||||
import type { Granularity } from './granularity.type';
|
import type { Granularity } from './granularity.type';
|
||||||
import type { OrderWithAccount } from './order-with-account.type';
|
import type { OrderWithAccount } from './order-with-account.type';
|
||||||
@ -6,6 +7,7 @@ import type { RequestWithUser } from './request-with-user.type';
|
|||||||
|
|
||||||
export type {
|
export type {
|
||||||
AccessWithGranteeUser,
|
AccessWithGranteeUser,
|
||||||
|
AccountWithValue,
|
||||||
DateRange,
|
DateRange,
|
||||||
Granularity,
|
Granularity,
|
||||||
OrderWithAccount,
|
OrderWithAccount,
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
class="align-items-center justify-content-center"
|
class="align-items-center justify-content-center"
|
||||||
color="primary"
|
color="primary"
|
||||||
[routerLink]="['/portfolio', 'transactions']"
|
[routerLink]="['/portfolio', 'transactions']"
|
||||||
|
[queryParams]="{ createDialog: true }"
|
||||||
mat-button
|
mat-button
|
||||||
>
|
>
|
||||||
<span i18n>Time to add your first transaction.</span>
|
<span i18n>Time to add your first transaction.</span>
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
:host {
|
:host {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
&.has-border {
|
||||||
border: 1px solid rgba(var(--dark-dividers));
|
border: 1px solid rgba(var(--dark-dividers));
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
display: block;
|
}
|
||||||
|
|
||||||
gf-logo {
|
gf-logo {
|
||||||
opacity: 0.25;
|
opacity: 0.25;
|
||||||
|
@ -1,4 +1,9 @@
|
|||||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
HostBinding,
|
||||||
|
Input
|
||||||
|
} from '@angular/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'gf-no-transactions-info-indicator',
|
selector: 'gf-no-transactions-info-indicator',
|
||||||
@ -6,8 +11,8 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
|||||||
templateUrl: './no-transactions-info.component.html',
|
templateUrl: './no-transactions-info.component.html',
|
||||||
styleUrls: ['./no-transactions-info.component.scss']
|
styleUrls: ['./no-transactions-info.component.scss']
|
||||||
})
|
})
|
||||||
export class NoTransactionsInfoComponent implements OnInit {
|
export class NoTransactionsInfoComponent {
|
||||||
public constructor() {}
|
@HostBinding('class.has-border') @Input() hasBorder = true;
|
||||||
|
|
||||||
public ngOnInit() {}
|
public constructor() {}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ghostfolio",
|
"name": "ghostfolio",
|
||||||
"version": "1.72.0",
|
"version": "1.75.0",
|
||||||
"homepage": "https://ghostfol.io",
|
"homepage": "https://ghostfol.io",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
Reference in New Issue
Block a user