Compare commits
19 Commits
Author | SHA1 | Date | |
---|---|---|---|
80464c7846 | |||
74f4323903 | |||
127dbf9dcd | |||
66bdb374e8 | |||
4ad4fa2b30 | |||
1fd836194f | |||
2090db1199 | |||
053c7e591e | |||
9b5e350e3b | |||
378e57c3bc | |||
6765191a8c | |||
8438a45bcf | |||
30a64e7fc1 | |||
f2cb671c7f | |||
3f41e5c5de | |||
c1ad483f33 | |||
f3d961bc16 | |||
42b70ef568 | |||
77beaaba08 |
31
CHANGELOG.md
31
CHANGELOG.md
@ -5,6 +5,37 @@ 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/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## 2.80.0 - 2024-05-08
|
||||
|
||||
### Added
|
||||
|
||||
- Added the absolute change column to the holdings table on the home page
|
||||
|
||||
### Changed
|
||||
|
||||
- Increased the spacing around the floating action buttons (FAB)
|
||||
- Set the icon column of the activities table to stick at the beginning
|
||||
- Set the icon column of the holdings table to stick at the beginning
|
||||
- Increased the number of attempts of queue jobs from `10` to `12` (fail later)
|
||||
- Upgraded `ionicons` from version `7.3.0` to `7.4.0`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the position detail dialog open functionality when searching for a holding in the assistant
|
||||
|
||||
## 2.79.0 - 2024-05-04
|
||||
|
||||
### Changed
|
||||
|
||||
- Moved the holdings table to the holdings tab of the home page
|
||||
- Improved the performance labels (with and without currency effects) in the position detail dialog
|
||||
- Optimized the calculations of the portfolio details endpoint
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue with the benchmarks in the markets overview
|
||||
- Fixed an issue with the _Fear & Greed Index_ (market mood) in the markets overview
|
||||
|
||||
## 2.78.0 - 2024-05-02
|
||||
|
||||
### Added
|
||||
|
@ -142,7 +142,7 @@ docker compose --env-file ./.env -f docker/docker-compose.build.yml up -d
|
||||
|
||||
### Home Server Systems (Community)
|
||||
|
||||
Ghostfolio is available for various home server systems, including [CasaOS](https://github.com/bigbeartechworld/big-bear-casaos), Home Assistant, [Runtipi](https://www.runtipi.io/docs/apps-available), [TrueCharts](https://truecharts.org/charts/stable/ghostfolio), [Umbrel](https://apps.umbrel.com/app/ghostfolio), and [Unraid](https://unraid.net/community/apps?q=ghostfolio).
|
||||
Ghostfolio is available for various home server systems, including [CasaOS](https://github.com/bigbeartechworld/big-bear-casaos), [Home Assistant](https://github.com/lildude/ha-addon-ghostfolio), [Runtipi](https://www.runtipi.io/docs/apps-available), [TrueCharts](https://truecharts.org/charts/stable/ghostfolio), [Umbrel](https://apps.umbrel.com/app/ghostfolio), and [Unraid](https://unraid.net/community/apps?q=ghostfolio).
|
||||
|
||||
## Development
|
||||
|
||||
|
@ -1,5 +0,0 @@
|
||||
import { Position } from '@ghostfolio/common/interfaces';
|
||||
|
||||
export interface PortfolioPositions {
|
||||
positions: Position[];
|
||||
}
|
@ -52,7 +52,6 @@ import { Big } from 'big.js';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
|
||||
import { PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface';
|
||||
import { PortfolioPositions } from './interfaces/portfolio-positions.interface';
|
||||
import { PortfolioService } from './portfolio.service';
|
||||
|
||||
@Controller('portfolio')
|
||||
@ -165,21 +164,21 @@ export class PortfolioController {
|
||||
portfolioSummary = nullifyValuesInObject(summary, [
|
||||
'cash',
|
||||
'committedFunds',
|
||||
'currentGrossPerformance',
|
||||
'currentGrossPerformanceWithCurrencyEffect',
|
||||
'currentNetPerformance',
|
||||
'currentNetPerformanceWithCurrencyEffect',
|
||||
'currentNetWorth',
|
||||
'currentValue',
|
||||
'currentValueInBaseCurrency',
|
||||
'dividendInBaseCurrency',
|
||||
'emergencyFund',
|
||||
'excludedAccountsAndActivities',
|
||||
'fees',
|
||||
'filteredValueInBaseCurrency',
|
||||
'fireWealth',
|
||||
'grossPerformance',
|
||||
'grossPerformanceWithCurrencyEffect',
|
||||
'interest',
|
||||
'items',
|
||||
'liabilities',
|
||||
'netPerformance',
|
||||
'netPerformanceWithCurrencyEffect',
|
||||
'totalBuy',
|
||||
'totalInvestment',
|
||||
'totalSell',
|
||||
@ -449,10 +448,14 @@ export class PortfolioController {
|
||||
.div(performanceInformation.performance.totalInvestment)
|
||||
.toNumber(),
|
||||
valueInPercentage:
|
||||
performanceInformation.performance.currentValue === 0
|
||||
performanceInformation.performance.currentValueInBaseCurrency ===
|
||||
0
|
||||
? 0
|
||||
: new Big(value)
|
||||
.div(performanceInformation.performance.currentValue)
|
||||
.div(
|
||||
performanceInformation.performance
|
||||
.currentValueInBaseCurrency
|
||||
)
|
||||
.toNumber()
|
||||
};
|
||||
}
|
||||
@ -461,12 +464,12 @@ export class PortfolioController {
|
||||
performanceInformation.performance = nullifyValuesInObject(
|
||||
performanceInformation.performance,
|
||||
[
|
||||
'currentGrossPerformance',
|
||||
'currentGrossPerformanceWithCurrencyEffect',
|
||||
'currentNetPerformance',
|
||||
'currentNetPerformanceWithCurrencyEffect',
|
||||
'currentNetWorth',
|
||||
'currentValue',
|
||||
'currentValueInBaseCurrency',
|
||||
'grossPerformance',
|
||||
'grossPerformanceWithCurrencyEffect',
|
||||
'netPerformance',
|
||||
'netPerformanceWithCurrencyEffect',
|
||||
'totalInvestment'
|
||||
]
|
||||
);
|
||||
@ -483,39 +486,13 @@ export class PortfolioController {
|
||||
);
|
||||
performanceInformation.performance = nullifyValuesInObject(
|
||||
performanceInformation.performance,
|
||||
['currentNetPerformance', 'currentNetPerformancePercent']
|
||||
['netPerformance']
|
||||
);
|
||||
}
|
||||
|
||||
return performanceInformation;
|
||||
}
|
||||
|
||||
@Get('positions')
|
||||
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
||||
@UseInterceptors(RedactValuesInResponseInterceptor)
|
||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||
public async getPositions(
|
||||
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
|
||||
@Query('accounts') filterByAccounts?: string,
|
||||
@Query('assetClasses') filterByAssetClasses?: string,
|
||||
@Query('query') filterBySearchQuery?: string,
|
||||
@Query('range') dateRange: DateRange = 'max',
|
||||
@Query('tags') filterByTags?: string
|
||||
): Promise<PortfolioPositions> {
|
||||
const filters = this.apiService.buildFiltersFromQueryParams({
|
||||
filterByAccounts,
|
||||
filterByAssetClasses,
|
||||
filterBySearchQuery,
|
||||
filterByTags
|
||||
});
|
||||
|
||||
return this.portfolioService.getPositions({
|
||||
dateRange,
|
||||
filters,
|
||||
impersonationId
|
||||
});
|
||||
}
|
||||
|
||||
@Get('public/:accessId')
|
||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||
public async getPublic(
|
||||
|
@ -27,7 +27,7 @@ describe('PortfolioService', () => {
|
||||
portfolioService
|
||||
.getAnnualizedPerformancePercent({
|
||||
daysInMarket: NaN, // differenceInDays of date-fns returns NaN for the same day
|
||||
netPerformancePercent: new Big(0)
|
||||
netPerformancePercentage: new Big(0)
|
||||
})
|
||||
.toNumber()
|
||||
).toEqual(0);
|
||||
@ -36,7 +36,7 @@ describe('PortfolioService', () => {
|
||||
portfolioService
|
||||
.getAnnualizedPerformancePercent({
|
||||
daysInMarket: 0,
|
||||
netPerformancePercent: new Big(0)
|
||||
netPerformancePercentage: new Big(0)
|
||||
})
|
||||
.toNumber()
|
||||
).toEqual(0);
|
||||
@ -48,7 +48,7 @@ describe('PortfolioService', () => {
|
||||
portfolioService
|
||||
.getAnnualizedPerformancePercent({
|
||||
daysInMarket: 65, // < 1 year
|
||||
netPerformancePercent: new Big(0.1025)
|
||||
netPerformancePercentage: new Big(0.1025)
|
||||
})
|
||||
.toNumber()
|
||||
).toBeCloseTo(0.729705);
|
||||
@ -57,7 +57,7 @@ describe('PortfolioService', () => {
|
||||
portfolioService
|
||||
.getAnnualizedPerformancePercent({
|
||||
daysInMarket: 365, // 1 year
|
||||
netPerformancePercent: new Big(0.05)
|
||||
netPerformancePercentage: new Big(0.05)
|
||||
})
|
||||
.toNumber()
|
||||
).toBeCloseTo(0.05);
|
||||
@ -69,7 +69,7 @@ describe('PortfolioService', () => {
|
||||
portfolioService
|
||||
.getAnnualizedPerformancePercent({
|
||||
daysInMarket: 575, // > 1 year
|
||||
netPerformancePercent: new Big(0.2374)
|
||||
netPerformancePercentage: new Big(0.2374)
|
||||
})
|
||||
.toNumber()
|
||||
).toBeCloseTo(0.145);
|
||||
|
@ -208,16 +208,16 @@ export class PortfolioService {
|
||||
|
||||
public getAnnualizedPerformancePercent({
|
||||
daysInMarket,
|
||||
netPerformancePercent
|
||||
netPerformancePercentage
|
||||
}: {
|
||||
daysInMarket: number;
|
||||
netPerformancePercent: Big;
|
||||
netPerformancePercentage: Big;
|
||||
}): Big {
|
||||
if (isNumber(daysInMarket) && daysInMarket > 0) {
|
||||
const exponent = new Big(365).div(daysInMarket).toNumber();
|
||||
|
||||
return new Big(
|
||||
Math.pow(netPerformancePercent.plus(1).toNumber(), exponent)
|
||||
Math.pow(netPerformancePercentage.plus(1).toNumber(), exponent)
|
||||
).minus(1);
|
||||
}
|
||||
|
||||
@ -360,7 +360,7 @@ export class PortfolioService {
|
||||
userId,
|
||||
calculationType: PerformanceCalculationType.TWR,
|
||||
currency: userCurrency,
|
||||
hasFilters: filters?.length > 0,
|
||||
hasFilters: true, // disable cache
|
||||
isExperimentalFeatures:
|
||||
this.request.user?.Settings.settings.isExperimentalFeatures
|
||||
});
|
||||
@ -704,7 +704,7 @@ export class PortfolioService {
|
||||
|
||||
const dividendYieldPercent = this.getAnnualizedPerformancePercent({
|
||||
daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)),
|
||||
netPerformancePercent: timeWeightedInvestment.eq(0)
|
||||
netPerformancePercentage: timeWeightedInvestment.eq(0)
|
||||
? new Big(0)
|
||||
: dividendInBaseCurrency.div(timeWeightedInvestment)
|
||||
});
|
||||
@ -712,7 +712,9 @@ export class PortfolioService {
|
||||
const dividendYieldPercentWithCurrencyEffect =
|
||||
this.getAnnualizedPerformancePercent({
|
||||
daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)),
|
||||
netPerformancePercent: timeWeightedInvestmentWithCurrencyEffect.eq(0)
|
||||
netPerformancePercentage: timeWeightedInvestmentWithCurrencyEffect.eq(
|
||||
0
|
||||
)
|
||||
? new Big(0)
|
||||
: dividendInBaseCurrency.div(
|
||||
timeWeightedInvestmentWithCurrencyEffect
|
||||
@ -1108,16 +1110,16 @@ export class PortfolioService {
|
||||
firstOrderDate: undefined,
|
||||
hasErrors: false,
|
||||
performance: {
|
||||
currentGrossPerformance: 0,
|
||||
currentGrossPerformancePercent: 0,
|
||||
currentGrossPerformancePercentWithCurrencyEffect: 0,
|
||||
currentGrossPerformanceWithCurrencyEffect: 0,
|
||||
currentNetPerformance: 0,
|
||||
currentNetPerformancePercent: 0,
|
||||
currentNetPerformancePercentWithCurrencyEffect: 0,
|
||||
currentNetPerformanceWithCurrencyEffect: 0,
|
||||
currentNetWorth: 0,
|
||||
currentValue: 0,
|
||||
currentValueInBaseCurrency: 0,
|
||||
grossPerformance: 0,
|
||||
grossPerformancePercentage: 0,
|
||||
grossPerformancePercentageWithCurrencyEffect: 0,
|
||||
grossPerformanceWithCurrencyEffect: 0,
|
||||
netPerformance: 0,
|
||||
netPerformancePercentage: 0,
|
||||
netPerformancePercentageWithCurrencyEffect: 0,
|
||||
netPerformanceWithCurrencyEffect: 0,
|
||||
totalInvestment: 0
|
||||
}
|
||||
};
|
||||
@ -1152,9 +1154,9 @@ export class PortfolioService {
|
||||
|
||||
let currentNetPerformance = netPerformance;
|
||||
|
||||
let currentNetPerformancePercent = netPerformancePercentage;
|
||||
let currentNetPerformancePercentage = netPerformancePercentage;
|
||||
|
||||
let currentNetPerformancePercentWithCurrencyEffect =
|
||||
let currentNetPerformancePercentageWithCurrencyEffect =
|
||||
netPerformancePercentageWithCurrencyEffect;
|
||||
|
||||
let currentNetPerformanceWithCurrencyEffect =
|
||||
@ -1173,11 +1175,11 @@ export class PortfolioService {
|
||||
if (itemOfToday) {
|
||||
currentNetPerformance = new Big(itemOfToday.netPerformance);
|
||||
|
||||
currentNetPerformancePercent = new Big(
|
||||
currentNetPerformancePercentage = new Big(
|
||||
itemOfToday.netPerformanceInPercentage
|
||||
).div(100);
|
||||
|
||||
currentNetPerformancePercentWithCurrencyEffect = new Big(
|
||||
currentNetPerformancePercentageWithCurrencyEffect = new Big(
|
||||
itemOfToday.netPerformanceInPercentageWithCurrencyEffect
|
||||
).div(100);
|
||||
|
||||
@ -1195,19 +1197,19 @@ export class PortfolioService {
|
||||
firstOrderDate: parseDate(items[0]?.date),
|
||||
performance: {
|
||||
currentNetWorth,
|
||||
currentGrossPerformance: grossPerformance.toNumber(),
|
||||
currentGrossPerformancePercent: grossPerformancePercentage.toNumber(),
|
||||
currentGrossPerformancePercentWithCurrencyEffect:
|
||||
currentValueInBaseCurrency: currentValueInBaseCurrency.toNumber(),
|
||||
grossPerformance: grossPerformance.toNumber(),
|
||||
grossPerformancePercentage: grossPerformancePercentage.toNumber(),
|
||||
grossPerformancePercentageWithCurrencyEffect:
|
||||
grossPerformancePercentageWithCurrencyEffect.toNumber(),
|
||||
currentGrossPerformanceWithCurrencyEffect:
|
||||
grossPerformanceWithCurrencyEffect:
|
||||
grossPerformanceWithCurrencyEffect.toNumber(),
|
||||
currentNetPerformance: currentNetPerformance.toNumber(),
|
||||
currentNetPerformancePercent: currentNetPerformancePercent.toNumber(),
|
||||
currentNetPerformancePercentWithCurrencyEffect:
|
||||
currentNetPerformancePercentWithCurrencyEffect.toNumber(),
|
||||
currentNetPerformanceWithCurrencyEffect:
|
||||
netPerformance: currentNetPerformance.toNumber(),
|
||||
netPerformancePercentage: currentNetPerformancePercentage.toNumber(),
|
||||
netPerformancePercentageWithCurrencyEffect:
|
||||
currentNetPerformancePercentageWithCurrencyEffect.toNumber(),
|
||||
netPerformanceWithCurrencyEffect:
|
||||
currentNetPerformanceWithCurrencyEffect.toNumber(),
|
||||
currentValue: currentValueInBaseCurrency.toNumber(),
|
||||
totalInvestment: totalInvestment.toNumber()
|
||||
}
|
||||
};
|
||||
@ -1604,11 +1606,6 @@ export class PortfolioService {
|
||||
userId = await this.getUserId(impersonationId, userId);
|
||||
const user = await this.userService.user({ id: userId });
|
||||
|
||||
const performanceInformation = await this.getPerformance({
|
||||
impersonationId,
|
||||
userId
|
||||
});
|
||||
|
||||
const { activities } = await this.orderService.getOrders({
|
||||
userCurrency,
|
||||
userId,
|
||||
@ -1626,6 +1623,19 @@ export class PortfolioService {
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
currentValueInBaseCurrency,
|
||||
grossPerformance,
|
||||
grossPerformancePercentage,
|
||||
grossPerformancePercentageWithCurrencyEffect,
|
||||
grossPerformanceWithCurrencyEffect,
|
||||
netPerformance,
|
||||
netPerformancePercentage,
|
||||
netPerformancePercentageWithCurrencyEffect,
|
||||
netPerformanceWithCurrencyEffect,
|
||||
totalInvestment
|
||||
} = await portfolioCalculator.getSnapshot();
|
||||
|
||||
const dividendInBaseCurrency =
|
||||
await portfolioCalculator.getDividendInBaseCurrency();
|
||||
|
||||
@ -1694,7 +1704,7 @@ export class PortfolioService {
|
||||
.toNumber();
|
||||
|
||||
const netWorth = new Big(balanceInBaseCurrency)
|
||||
.plus(performanceInformation.performance.currentValue)
|
||||
.plus(currentValueInBaseCurrency)
|
||||
.plus(valuables)
|
||||
.plus(excludedAccountsAndActivities)
|
||||
.minus(liabilities)
|
||||
@ -1704,21 +1714,18 @@ export class PortfolioService {
|
||||
|
||||
const annualizedPerformancePercent = this.getAnnualizedPerformancePercent({
|
||||
daysInMarket,
|
||||
netPerformancePercent: new Big(
|
||||
performanceInformation.performance.currentNetPerformancePercent
|
||||
)
|
||||
netPerformancePercentage: new Big(netPerformancePercentage)
|
||||
})?.toNumber();
|
||||
|
||||
const annualizedPerformancePercentWithCurrencyEffect =
|
||||
this.getAnnualizedPerformancePercent({
|
||||
daysInMarket,
|
||||
netPerformancePercent: new Big(
|
||||
performanceInformation.performance.currentNetPerformancePercentWithCurrencyEffect
|
||||
netPerformancePercentage: new Big(
|
||||
netPerformancePercentageWithCurrencyEffect
|
||||
)
|
||||
})?.toNumber();
|
||||
|
||||
return {
|
||||
...performanceInformation.performance,
|
||||
annualizedPerformancePercent,
|
||||
annualizedPerformancePercentWithCurrencyEffect,
|
||||
cash,
|
||||
@ -1727,6 +1734,7 @@ export class PortfolioService {
|
||||
totalBuy,
|
||||
totalSell,
|
||||
committedFunds: committedFunds.toNumber(),
|
||||
currentValueInBaseCurrency: currentValueInBaseCurrency.toNumber(),
|
||||
dividendInBaseCurrency: dividendInBaseCurrency.toNumber(),
|
||||
emergencyFund: {
|
||||
assets: emergencyFundPositionsValueInBaseCurrency,
|
||||
@ -1740,15 +1748,28 @@ export class PortfolioService {
|
||||
filteredValueInPercentage: netWorth
|
||||
? filteredValueInBaseCurrency.div(netWorth).toNumber()
|
||||
: undefined,
|
||||
fireWealth: new Big(performanceInformation.performance.currentValue)
|
||||
fireWealth: new Big(currentValueInBaseCurrency)
|
||||
.minus(emergencyFundPositionsValueInBaseCurrency)
|
||||
.toNumber(),
|
||||
grossPerformance: grossPerformance.toNumber(),
|
||||
grossPerformancePercentage: grossPerformancePercentage.toNumber(),
|
||||
grossPerformancePercentageWithCurrencyEffect:
|
||||
grossPerformancePercentageWithCurrencyEffect.toNumber(),
|
||||
grossPerformanceWithCurrencyEffect:
|
||||
grossPerformanceWithCurrencyEffect.toNumber(),
|
||||
interest: interest.toNumber(),
|
||||
items: valuables.toNumber(),
|
||||
liabilities: liabilities.toNumber(),
|
||||
netPerformance: netPerformance.toNumber(),
|
||||
netPerformancePercentage: netPerformancePercentage.toNumber(),
|
||||
netPerformancePercentageWithCurrencyEffect:
|
||||
netPerformancePercentageWithCurrencyEffect.toNumber(),
|
||||
netPerformanceWithCurrencyEffect:
|
||||
netPerformanceWithCurrencyEffect.toNumber(),
|
||||
ordersCount: activities.filter(({ type }) => {
|
||||
return type === 'BUY' || type === 'SELL';
|
||||
return ['BUY', 'SELL'].includes(type);
|
||||
}).length,
|
||||
totalInvestment: totalInvestment.toNumber(),
|
||||
totalValueInBaseCurrency: netWorth
|
||||
};
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ export class PortfolioChangedListener {
|
||||
@OnEvent(PortfolioChangedEvent.getName())
|
||||
handlePortfolioChangedEvent(event: PortfolioChangedEvent) {
|
||||
Logger.log(
|
||||
`Portfolio of user with id ${event.getUserId()} has changed`,
|
||||
`Portfolio of user '${event.getUserId()}' has changed`,
|
||||
'PortfolioChangedListener'
|
||||
);
|
||||
|
||||
|
@ -38,6 +38,7 @@ import { CreateAssetProfileDialogParams } from './create-asset-profile-dialog/in
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'has-fab' },
|
||||
selector: 'gf-admin-market-data',
|
||||
styleUrls: ['./admin-market-data.scss'],
|
||||
templateUrl: './admin-market-data.html'
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
|
||||
import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component';
|
||||
import { PositionDetailDialogParams } from '@ghostfolio/client/components/position-detail-dialog/interfaces/interfaces';
|
||||
import { PositionDetailDialog } from '@ghostfolio/client/components/position-detail-dialog/position-detail-dialog.component';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { Position, User } from '@ghostfolio/common/interfaces';
|
||||
import { PortfolioPosition, User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { DateRange } from '@ghostfolio/common/types';
|
||||
import { HoldingType, ToggleOption } from '@ghostfolio/common/types';
|
||||
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
@ -15,19 +15,21 @@ import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { PositionDetailDialogParams } from '../position/position-detail-dialog/interfaces/interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'gf-home-holdings',
|
||||
styleUrls: ['./home-holdings.scss'],
|
||||
templateUrl: './home-holdings.html'
|
||||
})
|
||||
export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
||||
public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS;
|
||||
public deviceType: string;
|
||||
public hasImpersonationId: boolean;
|
||||
public hasPermissionToCreateOrder: boolean;
|
||||
public positions: Position[];
|
||||
public holdings: PortfolioPosition[];
|
||||
public holdingType: HoldingType = 'ACTIVE';
|
||||
public holdingTypeOptions: ToggleOption[] = [
|
||||
{ label: $localize`Active`, value: 'ACTIVE' },
|
||||
{ label: $localize`Closed`, value: 'CLOSED' }
|
||||
];
|
||||
public user: User;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
@ -56,6 +58,17 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnInit() {
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
|
||||
this.impersonationStorageService
|
||||
.onChangeHasImpersonation()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((impersonationId) => {
|
||||
this.hasImpersonationId = !!impersonationId;
|
||||
});
|
||||
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
@ -68,37 +81,32 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
||||
permissions.createOrder
|
||||
);
|
||||
|
||||
this.update();
|
||||
this.holdings = undefined;
|
||||
|
||||
this.fetchHoldings()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ holdings }) => {
|
||||
this.holdings = holdings;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnInit() {
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
public onChangeHoldingType(aHoldingType: HoldingType) {
|
||||
this.holdingType = aHoldingType;
|
||||
|
||||
this.impersonationStorageService
|
||||
.onChangeHasImpersonation()
|
||||
this.holdings = undefined;
|
||||
|
||||
this.fetchHoldings()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((impersonationId) => {
|
||||
this.hasImpersonationId = !!impersonationId;
|
||||
});
|
||||
}
|
||||
.subscribe(({ holdings }) => {
|
||||
this.holdings = holdings;
|
||||
|
||||
public onChangeDateRange(dateRange: DateRange) {
|
||||
this.dataService
|
||||
.putUserSetting({ dateRange })
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
this.userService.remove();
|
||||
|
||||
this.userService
|
||||
.get()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((user) => {
|
||||
this.user = user;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
@ -107,6 +115,19 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
private fetchHoldings() {
|
||||
const filters = this.userService.getFilters();
|
||||
|
||||
if (this.holdingType === 'CLOSED') {
|
||||
filters.push({ id: 'CLOSED', type: 'HOLDING_TYPE' });
|
||||
}
|
||||
|
||||
return this.dataService.fetchPortfolioHoldings({
|
||||
filters,
|
||||
range: this.user?.settings?.dateRange
|
||||
});
|
||||
}
|
||||
|
||||
private openPositionDialog({
|
||||
dataSource,
|
||||
symbol
|
||||
@ -147,19 +168,4 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private update() {
|
||||
this.positions = undefined;
|
||||
|
||||
this.dataService
|
||||
.fetchPositions({ range: this.user?.settings?.dateRange })
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ positions }) => {
|
||||
this.positions = positions;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
@ -1,27 +1,38 @@
|
||||
<div class="container justify-content-center p-3">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="align-items-center col-xs-12 col-md-8 offset-md-2">
|
||||
<mat-card appearance="outlined">
|
||||
<mat-card-content class="p-0">
|
||||
<gf-positions
|
||||
[baseCurrency]="user?.settings?.baseCurrency"
|
||||
[deviceType]="deviceType"
|
||||
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
||||
[locale]="user?.settings?.locale"
|
||||
[positions]="positions"
|
||||
[range]="user?.settings?.dateRange"
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<div *ngIf="hasPermissionToCreateOrder" class="text-center">
|
||||
<a
|
||||
class="mt-3"
|
||||
i18n
|
||||
mat-stroked-button
|
||||
[routerLink]="['/portfolio', 'activities']"
|
||||
>Manage Activities</a
|
||||
>
|
||||
<div class="col">
|
||||
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Holdings</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg">
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-toggle
|
||||
class="d-none d-lg-block"
|
||||
[defaultValue]="holdingType"
|
||||
[isLoading]="false"
|
||||
[options]="holdingTypeOptions"
|
||||
(change)="onChangeHoldingType($event.value)"
|
||||
/>
|
||||
</div>
|
||||
<gf-holdings-table
|
||||
[baseCurrency]="user?.settings?.baseCurrency"
|
||||
[deviceType]="deviceType"
|
||||
[hasPermissionToCreateActivity]="hasPermissionToCreateOrder"
|
||||
[holdings]="holdings"
|
||||
[locale]="user?.settings?.locale"
|
||||
/>
|
||||
@if (hasPermissionToCreateOrder && holdings?.length > 0) {
|
||||
<div class="text-center">
|
||||
<a
|
||||
class="mt-3"
|
||||
i18n
|
||||
mat-stroked-button
|
||||
[routerLink]="['/portfolio', 'activities']"
|
||||
>Manage Activities</a
|
||||
>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,11 +1,9 @@
|
||||
import { GfPositionDetailDialogModule } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.module';
|
||||
import { GfPositionsModule } from '@ghostfolio/client/components/positions/positions.module';
|
||||
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
||||
import { GfHoldingsTableComponent } from '@ghostfolio/ui/holdings-table';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { HomeHoldingsComponent } from './home-holdings.component';
|
||||
@ -14,11 +12,9 @@ import { HomeHoldingsComponent } from './home-holdings.component';
|
||||
declarations: [HomeHoldingsComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfPositionDetailDialogModule,
|
||||
GfPositionsModule,
|
||||
GfHoldingsTableComponent,
|
||||
GfToggleModule,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
RouterModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
|
@ -11,7 +11,7 @@
|
||||
[colorScheme]="user?.settings?.colorScheme"
|
||||
[historicalDataItems]="historicalDataItems"
|
||||
[isAnimated]="true"
|
||||
[locale]="user?.settings?.locale"
|
||||
[locale]="user?.settings?.locale || undefined"
|
||||
[showXAxis]="true"
|
||||
[showYAxis]="true"
|
||||
[yMax]="100"
|
||||
@ -30,7 +30,7 @@
|
||||
<div class="col-xs-12 col-md-8 offset-md-2">
|
||||
<gf-benchmark
|
||||
[benchmarks]="benchmarks"
|
||||
[locale]="user?.settings?.locale"
|
||||
[locale]="user?.settings?.locale || undefined"
|
||||
[user]="user"
|
||||
/>
|
||||
<ngx-skeleton-loader
|
||||
|
@ -41,9 +41,7 @@
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="
|
||||
isLoading
|
||||
? undefined
|
||||
: performance?.currentNetPerformanceWithCurrencyEffect
|
||||
isLoading ? undefined : performance?.netPerformanceWithCurrencyEffect
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
@ -55,7 +53,7 @@
|
||||
[value]="
|
||||
isLoading
|
||||
? undefined
|
||||
: performance?.currentNetPerformancePercentWithCurrencyEffect
|
||||
: performance?.netPerformancePercentageWithCurrencyEffect
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
@ -49,12 +49,12 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
|
||||
this.value.nativeElement.innerHTML = '';
|
||||
}
|
||||
} else {
|
||||
if (isNumber(this.performance?.currentValue)) {
|
||||
new CountUp('value', this.performance?.currentValue, {
|
||||
if (isNumber(this.performance?.currentValueInBaseCurrency)) {
|
||||
new CountUp('value', this.performance?.currentValueInBaseCurrency, {
|
||||
decimal: getNumberFormatDecimal(this.locale),
|
||||
decimalPlaces:
|
||||
this.deviceType === 'mobile' &&
|
||||
this.performance?.currentValue >= 100000
|
||||
this.performance?.currentValueInBaseCurrency >= 100000
|
||||
? 0
|
||||
: 2,
|
||||
duration: 1,
|
||||
@ -63,8 +63,7 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
|
||||
} else if (this.showDetails === false) {
|
||||
new CountUp(
|
||||
'value',
|
||||
this.performance?.currentNetPerformancePercentWithCurrencyEffect *
|
||||
100,
|
||||
this.performance?.netPerformancePercentageWithCurrencyEffect * 100,
|
||||
{
|
||||
decimal: getNumberFormatDecimal(this.locale),
|
||||
decimalPlaces: 2,
|
||||
|
@ -9,9 +9,19 @@
|
||||
class="flex-nowrap px-3 py-1 row"
|
||||
[hidden]="summary?.ordersCount === null"
|
||||
>
|
||||
<div class="flex-grow-1 ml-3 text-truncate" i18n>
|
||||
<div class="d-flex flex-grow-1 ml-3 text-truncate">
|
||||
{{ summary?.ordersCount }}
|
||||
{summary?.ordersCount, plural, =1 {transaction} other {transactions}}
|
||||
<ng-container i18n>{summary?.ordersCount, plural,
|
||||
=1 {activity}
|
||||
other {activities}
|
||||
}</ng-container>
|
||||
<span
|
||||
class="align-items-center d-flex ml-1"
|
||||
matTooltipPosition="above"
|
||||
[matTooltip]="buyAndSellActivitiesTooltip"
|
||||
>
|
||||
<ion-icon name="information-circle-outline" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@ -65,9 +75,7 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="
|
||||
isLoading
|
||||
? undefined
|
||||
: summary?.currentGrossPerformanceWithCurrencyEffect
|
||||
isLoading ? undefined : summary?.grossPerformanceWithCurrencyEffect
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
@ -91,7 +99,7 @@
|
||||
[value]="
|
||||
isLoading
|
||||
? undefined
|
||||
: summary?.currentGrossPerformancePercentWithCurrencyEffect
|
||||
: summary?.grossPerformancePercentageWithCurrencyEffect
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
@ -121,9 +129,7 @@
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="
|
||||
isLoading
|
||||
? undefined
|
||||
: summary?.currentNetPerformanceWithCurrencyEffect
|
||||
isLoading ? undefined : summary?.netPerformanceWithCurrencyEffect
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
@ -147,7 +153,7 @@
|
||||
[value]="
|
||||
isLoading
|
||||
? undefined
|
||||
: summary?.currentNetPerformancePercentWithCurrencyEffect
|
||||
: summary?.netPerformancePercentageWithCurrencyEffect
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
@ -164,7 +170,7 @@
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.currentValue"
|
||||
[value]="isLoading ? undefined : summary?.currentValueInBaseCurrency"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { getDateFnsLocale, getLocale } from '@ghostfolio/common/helper';
|
||||
import { PortfolioSummary } from '@ghostfolio/common/interfaces';
|
||||
import { translate } from '@ghostfolio/ui/i18n';
|
||||
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
@ -28,6 +29,9 @@ export class PortfolioSummaryComponent implements OnChanges, OnInit {
|
||||
|
||||
@Output() emergencyFundChanged = new EventEmitter<number>();
|
||||
|
||||
public buyAndSellActivitiesTooltip = translate(
|
||||
'BUY_AND_SELL_ACTIVITIES_TOOLTIP'
|
||||
);
|
||||
public timeInMarket: string;
|
||||
|
||||
public constructor() {}
|
||||
|
@ -2,13 +2,14 @@ import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
|
||||
import { PortfolioSummaryComponent } from './portfolio-summary.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [PortfolioSummaryComponent],
|
||||
exports: [PortfolioSummaryComponent],
|
||||
imports: [CommonModule, GfValueComponent],
|
||||
imports: [CommonModule, GfValueComponent, MatTooltipModule],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfPortfolioSummaryModule {}
|
||||
|
@ -56,6 +56,8 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
||||
public marketPrice: number;
|
||||
public maxPrice: number;
|
||||
public minPrice: number;
|
||||
public netPerformance: number;
|
||||
public netPerformancePercent: number;
|
||||
public netPerformancePercentWithCurrencyEffect: number;
|
||||
public netPerformanceWithCurrencyEffect: number;
|
||||
public quantity: number;
|
||||
@ -104,6 +106,8 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
||||
marketPrice,
|
||||
maxPrice,
|
||||
minPrice,
|
||||
netPerformance,
|
||||
netPerformancePercent,
|
||||
netPerformancePercentWithCurrencyEffect,
|
||||
netPerformanceWithCurrencyEffect,
|
||||
orders,
|
||||
@ -126,15 +130,15 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
||||
this.feeInBaseCurrency = feeInBaseCurrency;
|
||||
this.firstBuyDate = firstBuyDate;
|
||||
this.historicalDataItems = historicalData.map(
|
||||
(historicalDataItem) => {
|
||||
({ averagePrice, date, marketPrice }) => {
|
||||
this.benchmarkDataItems.push({
|
||||
date: historicalDataItem.date,
|
||||
value: historicalDataItem.averagePrice
|
||||
date,
|
||||
value: averagePrice
|
||||
});
|
||||
|
||||
return {
|
||||
date: historicalDataItem.date,
|
||||
value: historicalDataItem.marketPrice
|
||||
date,
|
||||
value: marketPrice
|
||||
};
|
||||
}
|
||||
);
|
||||
@ -142,6 +146,8 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
||||
this.marketPrice = marketPrice;
|
||||
this.maxPrice = maxPrice;
|
||||
this.minPrice = minPrice;
|
||||
this.netPerformance = netPerformance;
|
||||
this.netPerformancePercent = netPerformancePercent;
|
||||
this.netPerformancePercentWithCurrencyEffect =
|
||||
netPerformancePercentWithCurrencyEffect;
|
||||
this.netPerformanceWithCurrencyEffect =
|
@ -37,27 +37,58 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6 mb-3">
|
||||
<gf-value
|
||||
i18n
|
||||
size="medium"
|
||||
[colorizeSign]="true"
|
||||
[isCurrency]="true"
|
||||
[locale]="data.locale"
|
||||
[unit]="data.baseCurrency"
|
||||
[value]="netPerformanceWithCurrencyEffect"
|
||||
>Change</gf-value
|
||||
>
|
||||
@if (
|
||||
SymbolProfile?.currency &&
|
||||
data.baseCurrency !== SymbolProfile?.currency
|
||||
) {
|
||||
<gf-value
|
||||
i18n
|
||||
size="medium"
|
||||
[colorizeSign]="true"
|
||||
[isCurrency]="true"
|
||||
[locale]="data.locale"
|
||||
[unit]="data.baseCurrency"
|
||||
[value]="netPerformanceWithCurrencyEffect"
|
||||
>Change with currency effect</gf-value
|
||||
>
|
||||
} @else {
|
||||
<gf-value
|
||||
i18n
|
||||
size="medium"
|
||||
[colorizeSign]="true"
|
||||
[isCurrency]="true"
|
||||
[locale]="data.locale"
|
||||
[unit]="data.baseCurrency"
|
||||
[value]="netPerformance"
|
||||
>Change</gf-value
|
||||
>
|
||||
}
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<gf-value
|
||||
i18n
|
||||
size="medium"
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="data.locale"
|
||||
[value]="netPerformancePercentWithCurrencyEffect"
|
||||
>Performance</gf-value
|
||||
>
|
||||
@if (
|
||||
SymbolProfile?.currency &&
|
||||
data.baseCurrency !== SymbolProfile?.currency
|
||||
) {
|
||||
<gf-value
|
||||
i18n
|
||||
size="medium"
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="data.locale"
|
||||
[value]="netPerformancePercentWithCurrencyEffect"
|
||||
>Performance with currency effect</gf-value
|
||||
>
|
||||
} @else {
|
||||
<gf-value
|
||||
i18n
|
||||
size="medium"
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="data.locale"
|
||||
[value]="netPerformancePercent"
|
||||
>Performance</gf-value
|
||||
>
|
||||
}
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<gf-value
|
@ -1,72 +0,0 @@
|
||||
<div class="container p-0">
|
||||
<div class="flex-nowrap no-gutters row">
|
||||
<a
|
||||
class="d-flex p-3 w-100"
|
||||
[ngClass]="{ 'cursor-default': isLoading }"
|
||||
[queryParams]="{
|
||||
dataSource: position?.dataSource,
|
||||
positionDetailDialog: true,
|
||||
symbol: position?.symbol
|
||||
}"
|
||||
[routerLink]="[]"
|
||||
>
|
||||
<div class="d-flex mr-2">
|
||||
<gf-trend-indicator
|
||||
class="d-flex"
|
||||
size="large"
|
||||
[isLoading]="isLoading"
|
||||
[marketState]="position?.marketState"
|
||||
[range]="range"
|
||||
[value]="position?.netPerformancePercentageWithCurrencyEffect"
|
||||
/>
|
||||
</div>
|
||||
<div *ngIf="isLoading" class="flex-grow-1">
|
||||
<ngx-skeleton-loader
|
||||
animation="pulse"
|
||||
class="mb-1"
|
||||
[theme]="{
|
||||
height: '1.2rem',
|
||||
width: '12rem'
|
||||
}"
|
||||
/>
|
||||
<ngx-skeleton-loader
|
||||
animation="pulse"
|
||||
[theme]="{
|
||||
height: '1rem',
|
||||
width: '8rem'
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<div *ngIf="!isLoading" class="flex-grow-1 text-truncate">
|
||||
<div class="h6 m-0 text-truncate">{{ position?.name }}</div>
|
||||
<div class="d-flex">
|
||||
<small class="text-muted">{{ position?.symbol | gfSymbol }}</small>
|
||||
</div>
|
||||
<div class="d-flex mt-1">
|
||||
<gf-value
|
||||
class="mr-3"
|
||||
[colorizeSign]="true"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="position?.netPerformanceWithCurrencyEffect"
|
||||
/>
|
||||
<gf-value
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="locale"
|
||||
[value]="position?.netPerformancePercentageWithCurrencyEffect"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="align-items-center d-flex">
|
||||
<ion-icon
|
||||
*ngIf="!isLoading"
|
||||
class="chevron text-muted"
|
||||
name="chevron-forward-outline"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
@ -1,13 +0,0 @@
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
.container {
|
||||
gf-trend-indicator {
|
||||
padding-top: 0.15rem;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
opacity: 0.33;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||
import { getLocale } from '@ghostfolio/common/helper';
|
||||
import { Position } from '@ghostfolio/common/interfaces';
|
||||
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'gf-position',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: './position.component.html',
|
||||
styleUrls: ['./position.component.scss']
|
||||
})
|
||||
export class PositionComponent implements OnDestroy, OnInit {
|
||||
@Input() baseCurrency: string;
|
||||
@Input() deviceType: string;
|
||||
@Input() isLoading: boolean;
|
||||
@Input() locale = getLocale();
|
||||
@Input() position: Position;
|
||||
@Input() range: string;
|
||||
|
||||
public unknownKey = UNKNOWN_KEY;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor() {}
|
||||
|
||||
public ngOnInit() {}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||
import { GfTrendIndicatorComponent } from '@ghostfolio/ui/trend-indicator';
|
||||
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
|
||||
import { GfPositionDetailDialogModule } from './position-detail-dialog/position-detail-dialog.module';
|
||||
import { PositionComponent } from './position.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [PositionComponent],
|
||||
exports: [PositionComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfPositionDetailDialogModule,
|
||||
GfSymbolModule,
|
||||
GfTrendIndicatorComponent,
|
||||
GfValueComponent,
|
||||
MatDialogModule,
|
||||
NgxSkeletonLoaderModule,
|
||||
RouterModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfPositionModule {}
|
@ -1,35 +0,0 @@
|
||||
<div class="container p-0">
|
||||
<div class="row no-gutters">
|
||||
<div class="col">
|
||||
<ng-container *ngIf="positions === undefined">
|
||||
<gf-position [isLoading]="true" />
|
||||
</ng-container>
|
||||
<ng-container *ngIf="positions !== undefined">
|
||||
<ng-container *ngIf="hasPositions">
|
||||
<gf-position
|
||||
*ngFor="let position of positionsWithPriority"
|
||||
[baseCurrency]="baseCurrency"
|
||||
[deviceType]="deviceType"
|
||||
[locale]="locale"
|
||||
[position]="position"
|
||||
[range]="range"
|
||||
/>
|
||||
<gf-position
|
||||
*ngFor="let position of positionsRest"
|
||||
[baseCurrency]="baseCurrency"
|
||||
[deviceType]="deviceType"
|
||||
[locale]="locale"
|
||||
[position]="position"
|
||||
[range]="range"
|
||||
/>
|
||||
</ng-container>
|
||||
<div
|
||||
*ngIf="hasPermissionToCreateOrder && !hasPositions"
|
||||
class="p-3 text-center"
|
||||
>
|
||||
<gf-no-transactions-info-indicator [hasBorder]="false" />
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,17 +0,0 @@
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
gf-position {
|
||||
&:nth-child(even) {
|
||||
background-color: rgba(0, 0, 0, var(--gf-theme-alpha-hover));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
gf-position {
|
||||
&:nth-child(even) {
|
||||
background-color: rgba(255, 255, 255, var(--gf-theme-alpha-hover));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
import { getLocale } from '@ghostfolio/common/helper';
|
||||
import { Position } from '@ghostfolio/common/interfaces';
|
||||
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'gf-positions',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: './positions.component.html',
|
||||
styleUrls: ['./positions.component.scss']
|
||||
})
|
||||
export class PositionsComponent implements OnChanges, OnInit {
|
||||
@Input() baseCurrency: string;
|
||||
@Input() deviceType: string;
|
||||
@Input() hasPermissionToCreateOrder: boolean;
|
||||
@Input() locale = getLocale();
|
||||
@Input() positions: Position[];
|
||||
@Input() range: string;
|
||||
|
||||
public hasPositions: boolean;
|
||||
public positionsRest: Position[] = [];
|
||||
public positionsWithPriority: Position[] = [];
|
||||
|
||||
public constructor() {}
|
||||
|
||||
public ngOnInit() {}
|
||||
|
||||
public ngOnChanges() {
|
||||
if (this.positions) {
|
||||
this.hasPositions = this.positions.length > 0;
|
||||
|
||||
if (!this.hasPositions) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.positionsRest = [];
|
||||
this.positionsWithPriority = [];
|
||||
|
||||
for (const portfolioPosition of this.positions) {
|
||||
if (portfolioPosition.marketState === 'open' || this.range !== '1d') {
|
||||
// Only show positions where the market is open in today's view
|
||||
this.positionsWithPriority.push(portfolioPosition);
|
||||
} else {
|
||||
this.positionsRest.push(portfolioPosition);
|
||||
}
|
||||
}
|
||||
|
||||
this.positionsRest.sort((a, b) =>
|
||||
(a.name || a.symbol)?.toLowerCase() >
|
||||
(b.name || b.symbol)?.toLowerCase()
|
||||
? 1
|
||||
: -1
|
||||
);
|
||||
this.positionsWithPriority.sort((a, b) =>
|
||||
(a.name || a.symbol)?.toLowerCase() >
|
||||
(b.name || b.symbol)?.toLowerCase()
|
||||
? 1
|
||||
: -1
|
||||
);
|
||||
} else {
|
||||
this.hasPositions = false;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
import { GfNoTransactionsInfoComponent } from '@ghostfolio/ui/no-transactions-info';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
|
||||
import { GfPositionModule } from '../position/position.module';
|
||||
import { PositionsComponent } from './positions.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [PositionsComponent],
|
||||
exports: [PositionsComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfNoTransactionsInfoComponent,
|
||||
GfPositionModule,
|
||||
MatButtonModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfPositionsModule {}
|
@ -6,7 +6,6 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
|
||||
import { GfPositionModule } from '../position/position.module';
|
||||
import { RulesComponent } from './rules.component';
|
||||
|
||||
@NgModule({
|
||||
@ -15,7 +14,6 @@ import { RulesComponent } from './rules.component';
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfNoTransactionsInfoComponent,
|
||||
GfPositionModule,
|
||||
GfRuleModule,
|
||||
MatButtonModule,
|
||||
MatCardModule
|
||||
|
@ -21,6 +21,7 @@ import { CreateOrUpdateAccessDialog } from './create-or-update-access-dialog/cre
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'has-fab' },
|
||||
selector: 'gf-user-account-access',
|
||||
styleUrls: ['./user-account-access.scss'],
|
||||
templateUrl: './user-account-access.html'
|
||||
|
@ -77,7 +77,7 @@
|
||||
mat-icon-button
|
||||
title="Follow Ghostfolio on X (formerly Twitter)"
|
||||
>
|
||||
<span class="line-height-1 text-center w-100">𝕏</span>
|
||||
<ion-icon name="logo-x" />
|
||||
</a>
|
||||
<a
|
||||
*ngIf="user?.subscription?.type === 'Premium'"
|
||||
|
@ -21,7 +21,7 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog/c
|
||||
import { TransferBalanceDialog } from './transfer-balance/transfer-balance-dialog.component';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
host: { class: 'has-fab page' },
|
||||
selector: 'gf-accounts-page',
|
||||
styleUrls: ['./accounts-page.scss'],
|
||||
templateUrl: './accounts-page.html'
|
||||
|
@ -30,8 +30,10 @@
|
||||
systems, including
|
||||
<a href="https://github.com/bigbeartechworld/big-bear-casaos"
|
||||
>CasaOS</a
|
||||
>, Home Assistant,
|
||||
<a href="https://www.runtipi.io/docs/apps-available">Runtipi</a>,
|
||||
>,
|
||||
<a href="https://github.com/lildude/ha-addon-ghostfolio"
|
||||
>Home Assistant</a
|
||||
>, <a href="https://www.runtipi.io/docs/apps-available">Runtipi</a>,
|
||||
<a href="https://truecharts.org/charts/stable/ghostfolio"
|
||||
>TrueCharts</a
|
||||
>, <a href="https://apps.umbrel.com/app/ghostfolio">Umbrel</a>, and
|
||||
|
@ -22,6 +22,11 @@ const routes: Routes = [
|
||||
component: HomeHoldingsComponent,
|
||||
title: $localize`Holdings`
|
||||
},
|
||||
{
|
||||
path: 'holdings',
|
||||
component: HomeHoldingsComponent,
|
||||
title: $localize`Holdings`
|
||||
},
|
||||
{
|
||||
path: 'summary',
|
||||
component: HomeSummaryComponent,
|
||||
|
@ -1,7 +1,14 @@
|
||||
import { PositionDetailDialogParams } from '@ghostfolio/client/components/position-detail-dialog/interfaces/interfaces';
|
||||
import { PositionDetailDialog } from '@ghostfolio/client/components/position-detail-dialog/position-detail-dialog.component';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { TabConfiguration, User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { DataSource } from '@prisma/client';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
@ -14,6 +21,7 @@ import { takeUntil } from 'rxjs/operators';
|
||||
})
|
||||
export class HomePageComponent implements OnDestroy, OnInit {
|
||||
public deviceType: string;
|
||||
public hasImpersonationId: boolean;
|
||||
public tabs: TabConfiguration[] = [];
|
||||
public user: User;
|
||||
|
||||
@ -22,8 +30,27 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
||||
public constructor(
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private deviceService: DeviceDetectorService,
|
||||
private dialog: MatDialog,
|
||||
private impersonationStorageService: ImpersonationStorageService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private userService: UserService
|
||||
) {
|
||||
this.route.queryParams
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((params) => {
|
||||
if (
|
||||
params['dataSource'] &&
|
||||
params['positionDetailDialog'] &&
|
||||
params['symbol']
|
||||
) {
|
||||
this.openPositionDialog({
|
||||
dataSource: params['dataSource'],
|
||||
symbol: params['symbol']
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((state) => {
|
||||
@ -59,10 +86,58 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
||||
|
||||
public ngOnInit() {
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
|
||||
this.impersonationStorageService
|
||||
.onChangeHasImpersonation()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((impersonationId) => {
|
||||
this.hasImpersonationId = !!impersonationId;
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
private openPositionDialog({
|
||||
dataSource,
|
||||
symbol
|
||||
}: {
|
||||
dataSource: DataSource;
|
||||
symbol: string;
|
||||
}) {
|
||||
this.userService
|
||||
.get()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((user) => {
|
||||
this.user = user;
|
||||
|
||||
const dialogRef = this.dialog.open(PositionDetailDialog, {
|
||||
autoFocus: false,
|
||||
data: <PositionDetailDialogParams>{
|
||||
dataSource,
|
||||
symbol,
|
||||
baseCurrency: this.user?.settings?.baseCurrency,
|
||||
colorScheme: this.user?.settings?.colorScheme,
|
||||
deviceType: this.deviceType,
|
||||
hasImpersonationId: this.hasImpersonationId,
|
||||
hasPermissionToReportDataGlitch: hasPermission(
|
||||
this.user?.permissions,
|
||||
permissions.reportDataGlitch
|
||||
),
|
||||
locale: this.user?.settings?.locale
|
||||
},
|
||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||
});
|
||||
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
this.router.navigate(['.'], { relativeTo: this.route });
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
||||
import { PositionDetailDialogParams } from '@ghostfolio/client/components/position/position-detail-dialog/interfaces/interfaces';
|
||||
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
|
||||
import { PositionDetailDialogParams } from '@ghostfolio/client/components/position-detail-dialog/interfaces/interfaces';
|
||||
import { PositionDetailDialog } from '@ghostfolio/client/components/position-detail-dialog/position-detail-dialog.component';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { IcsService } from '@ghostfolio/client/services/ics/ics.service';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
@ -29,6 +29,7 @@ import { ImportActivitiesDialog } from './import-activities-dialog/import-activi
|
||||
import { ImportActivitiesDialogParams } from './import-activities-dialog/interfaces/interfaces';
|
||||
|
||||
@Component({
|
||||
host: { class: 'has-fab' },
|
||||
selector: 'gf-activities-page',
|
||||
styleUrls: ['./activities-page.scss'],
|
||||
templateUrl: './activities-page.html'
|
||||
|
@ -2,7 +2,7 @@ import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto
|
||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { ImportActivitiesService } from '@ghostfolio/client/services/import-activities.service';
|
||||
import { Position } from '@ghostfolio/common/interfaces';
|
||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||
|
||||
import {
|
||||
StepperOrientation,
|
||||
@ -43,7 +43,7 @@ export class ImportActivitiesDialog implements OnDestroy {
|
||||
public deviceType: string;
|
||||
public dialogTitle = $localize`Import Activities`;
|
||||
public errorMessages: string[] = [];
|
||||
public holdings: Position[] = [];
|
||||
public holdings: PortfolioPosition[] = [];
|
||||
public importStep: ImportStep = ImportStep.UPLOAD_FILE;
|
||||
public isLoading = false;
|
||||
public maxSafeInteger = Number.MAX_SAFE_INTEGER;
|
||||
@ -88,7 +88,7 @@ export class ImportActivitiesDialog implements OnDestroy {
|
||||
this.uniqueAssetForm.get('uniqueAsset').disable();
|
||||
|
||||
this.dataService
|
||||
.fetchPositions({
|
||||
.fetchPortfolioHoldings({
|
||||
filters: [
|
||||
{
|
||||
id: AssetClass.EQUITY,
|
||||
@ -98,8 +98,8 @@ export class ImportActivitiesDialog implements OnDestroy {
|
||||
range: 'max'
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ positions }) => {
|
||||
this.holdings = sortBy(positions, ({ name }) => {
|
||||
.subscribe(({ holdings }) => {
|
||||
this.holdings = sortBy(holdings, ({ name }) => {
|
||||
return name.toLowerCase();
|
||||
});
|
||||
this.uniqueAssetForm.get('uniqueAsset').enable();
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { AccountDetailDialog } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.component';
|
||||
import { AccountDetailDialogParams } from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces';
|
||||
import { PositionDetailDialogParams } from '@ghostfolio/client/components/position/position-detail-dialog/interfaces/interfaces';
|
||||
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
|
||||
import { PositionDetailDialogParams } from '@ghostfolio/client/components/position-detail-dialog/interfaces/interfaces';
|
||||
import { PositionDetailDialog } from '@ghostfolio/client/components/position-detail-dialog/position-detail-dialog.component';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
@ -103,7 +103,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
private router: Router,
|
||||
private userService: UserService
|
||||
) {
|
||||
route.queryParams
|
||||
this.route.queryParams
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((params) => {
|
||||
if (params['accountId'] && params['accountDetailDialog']) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { PositionDetailDialogParams } from '@ghostfolio/client/components/position/position-detail-dialog/interfaces/interfaces';
|
||||
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
|
||||
import { PositionDetailDialogParams } from '@ghostfolio/client/components/position-detail-dialog/interfaces/interfaces';
|
||||
import { PositionDetailDialog } from '@ghostfolio/client/components/position-detail-dialog/position-detail-dialog.component';
|
||||
import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
@ -8,7 +8,7 @@ import {
|
||||
HistoricalDataItem,
|
||||
PortfolioInvestments,
|
||||
PortfolioPerformance,
|
||||
Position,
|
||||
PortfolioPosition,
|
||||
User
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||
@ -35,7 +35,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
public benchmark: Partial<SymbolProfile>;
|
||||
public benchmarkDataItems: HistoricalDataItem[] = [];
|
||||
public benchmarks: Partial<SymbolProfile>[];
|
||||
public bottom3: Position[];
|
||||
public bottom3: PortfolioPosition[];
|
||||
public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS;
|
||||
public daysInMarket: number;
|
||||
public deviceType: string;
|
||||
@ -60,7 +60,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
public performanceDataItemsInPercentage: HistoricalDataItem[];
|
||||
public portfolioEvolutionDataLabel = $localize`Investment`;
|
||||
public streaks: PortfolioInvestments['streaks'];
|
||||
public top3: Position[];
|
||||
public top3: PortfolioPosition[];
|
||||
public unitCurrentStreak: string;
|
||||
public unitLongestStreak: string;
|
||||
public user: User;
|
||||
@ -80,7 +80,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
const { benchmarks } = this.dataService.fetchInfo();
|
||||
this.benchmarks = benchmarks;
|
||||
|
||||
route.queryParams
|
||||
this.route.queryParams
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((params) => {
|
||||
if (
|
||||
@ -308,23 +308,23 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
|
||||
this.dataService
|
||||
.fetchPositions({
|
||||
.fetchPortfolioHoldings({
|
||||
filters: this.userService.getFilters(),
|
||||
range: this.user?.settings?.dateRange
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ positions }) => {
|
||||
const positionsSorted = sortBy(
|
||||
positions.filter(({ netPerformancePercentageWithCurrencyEffect }) => {
|
||||
return isNumber(netPerformancePercentageWithCurrencyEffect);
|
||||
.subscribe(({ holdings }) => {
|
||||
const holdingsSorted = sortBy(
|
||||
holdings.filter(({ netPerformancePercentWithCurrencyEffect }) => {
|
||||
return isNumber(netPerformancePercentWithCurrencyEffect);
|
||||
}),
|
||||
'netPerformancePercentageWithCurrencyEffect'
|
||||
'netPerformancePercentWithCurrencyEffect'
|
||||
).reverse();
|
||||
|
||||
this.top3 = positionsSorted.slice(0, 3);
|
||||
this.top3 = holdingsSorted.slice(0, 3);
|
||||
|
||||
if (positions?.length > 3) {
|
||||
this.bottom3 = positionsSorted.slice(-3).reverse();
|
||||
if (holdings?.length > 3) {
|
||||
this.bottom3 = holdingsSorted.slice(-3).reverse();
|
||||
} else {
|
||||
this.bottom3 = [];
|
||||
}
|
||||
|
@ -42,7 +42,7 @@
|
||||
[value]="
|
||||
isLoadingInvestmentChart
|
||||
? undefined
|
||||
: performance?.currentNetPerformance
|
||||
: performance?.netPerformance
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
@ -61,7 +61,7 @@
|
||||
[value]="
|
||||
isLoadingInvestmentChart
|
||||
? undefined
|
||||
: performance?.currentNetPerformancePercent
|
||||
: performance?.netPerformancePercentage
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
@ -86,10 +86,10 @@
|
||||
[value]="
|
||||
isLoadingInvestmentChart
|
||||
? undefined
|
||||
: performance?.currentNetPerformance === null
|
||||
: performance?.netPerformance === null
|
||||
? null
|
||||
: performance?.currentNetPerformanceWithCurrencyEffect -
|
||||
performance?.currentNetPerformance
|
||||
: performance?.netPerformanceWithCurrencyEffect -
|
||||
performance?.netPerformance
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
@ -108,10 +108,10 @@
|
||||
[value]="
|
||||
isLoadingInvestmentChart
|
||||
? undefined
|
||||
: performance?.currentNetPerformancePercent === null
|
||||
: performance?.netPerformancePercentage === null
|
||||
? null
|
||||
: performance?.currentNetPerformancePercentWithCurrencyEffect -
|
||||
performance?.currentNetPerformancePercent
|
||||
: performance?.netPerformancePercentageWithCurrencyEffect -
|
||||
performance?.netPerformancePercentage
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
@ -131,7 +131,7 @@
|
||||
[value]="
|
||||
isLoadingInvestmentChart
|
||||
? undefined
|
||||
: performance?.currentNetPerformanceWithCurrencyEffect
|
||||
: performance?.netPerformanceWithCurrencyEffect
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
@ -150,7 +150,7 @@
|
||||
[value]="
|
||||
isLoadingInvestmentChart
|
||||
? undefined
|
||||
: performance?.currentNetPerformancePercentWithCurrencyEffect
|
||||
: performance?.netPerformancePercentageWithCurrencyEffect
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
@ -170,17 +170,17 @@
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<ol class="mb-0 ml-1 pl-3">
|
||||
<li *ngFor="let position of top3" class="py-1">
|
||||
<li *ngFor="let holding of top3" class="py-1">
|
||||
<a
|
||||
class="d-flex"
|
||||
[queryParams]="{
|
||||
dataSource: position.dataSource,
|
||||
dataSource: holding.dataSource,
|
||||
positionDetailDialog: true,
|
||||
symbol: position.symbol
|
||||
symbol: holding.symbol
|
||||
}"
|
||||
[routerLink]="[]"
|
||||
>
|
||||
<div class="flex-grow-1 mr-2">{{ position.name }}</div>
|
||||
<div class="flex-grow-1 mr-2">{{ holding.name }}</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
@ -188,9 +188,7 @@
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="
|
||||
position.netPerformancePercentageWithCurrencyEffect
|
||||
"
|
||||
[value]="holding.netPerformancePercentWithCurrencyEffect"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
@ -218,17 +216,17 @@
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<ol class="mb-0 ml-1 pl-3">
|
||||
<li *ngFor="let position of bottom3" class="py-1">
|
||||
<li *ngFor="let holding of bottom3" class="py-1">
|
||||
<a
|
||||
class="d-flex"
|
||||
[queryParams]="{
|
||||
dataSource: position.dataSource,
|
||||
dataSource: holding.dataSource,
|
||||
positionDetailDialog: true,
|
||||
symbol: position.symbol
|
||||
symbol: holding.symbol
|
||||
}"
|
||||
[routerLink]="[]"
|
||||
>
|
||||
<div class="flex-grow-1 mr-2">{{ position.name }}</div>
|
||||
<div class="flex-grow-1 mr-2">{{ holding.name }}</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
@ -236,9 +234,7 @@
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="
|
||||
position.netPerformancePercentageWithCurrencyEffect
|
||||
"
|
||||
[value]="holding.netPerformancePercentWithCurrencyEffect"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
|
@ -1,21 +0,0 @@
|
||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { HoldingsPageComponent } from './holdings-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
component: HoldingsPageComponent,
|
||||
path: '',
|
||||
title: $localize`Holdings`
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class HoldingsPageRoutingModule {}
|
@ -1,171 +0,0 @@
|
||||
import { PositionDetailDialogParams } from '@ghostfolio/client/components/position/position-detail-dialog/interfaces/interfaces';
|
||||
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { PortfolioPosition, User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { HoldingType, ToggleOption } from '@ghostfolio/common/types';
|
||||
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { DataSource } from '@prisma/client';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'gf-holdings-page',
|
||||
styleUrls: ['./holdings-page.scss'],
|
||||
templateUrl: './holdings-page.html'
|
||||
})
|
||||
export class HoldingsPageComponent implements OnDestroy, OnInit {
|
||||
public deviceType: string;
|
||||
public hasImpersonationId: boolean;
|
||||
public hasPermissionToCreateOrder: boolean;
|
||||
public holdings: PortfolioPosition[];
|
||||
public holdingType: HoldingType = 'ACTIVE';
|
||||
public holdingTypeOptions: ToggleOption[] = [
|
||||
{ label: $localize`Active`, value: 'ACTIVE' },
|
||||
{ label: $localize`Closed`, value: 'CLOSED' }
|
||||
];
|
||||
public user: User;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor(
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
private deviceService: DeviceDetectorService,
|
||||
private dialog: MatDialog,
|
||||
private impersonationStorageService: ImpersonationStorageService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private userService: UserService
|
||||
) {
|
||||
route.queryParams
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((params) => {
|
||||
if (
|
||||
params['dataSource'] &&
|
||||
params['positionDetailDialog'] &&
|
||||
params['symbol']
|
||||
) {
|
||||
this.openPositionDialog({
|
||||
dataSource: params['dataSource'],
|
||||
symbol: params['symbol']
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnInit() {
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
|
||||
this.impersonationStorageService
|
||||
.onChangeHasImpersonation()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((impersonationId) => {
|
||||
this.hasImpersonationId = !!impersonationId;
|
||||
});
|
||||
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((state) => {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
|
||||
this.hasPermissionToCreateOrder = hasPermission(
|
||||
this.user.permissions,
|
||||
permissions.createOrder
|
||||
);
|
||||
|
||||
this.holdings = undefined;
|
||||
|
||||
this.fetchHoldings()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ holdings }) => {
|
||||
this.holdings = holdings;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public onChangeHoldingType(aHoldingType: HoldingType) {
|
||||
this.holdingType = aHoldingType;
|
||||
|
||||
this.holdings = undefined;
|
||||
|
||||
this.fetchHoldings()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ holdings }) => {
|
||||
this.holdings = holdings;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
private fetchHoldings() {
|
||||
const filters = this.userService.getFilters();
|
||||
|
||||
if (this.holdingType === 'CLOSED') {
|
||||
filters.push({ id: 'CLOSED', type: 'HOLDING_TYPE' });
|
||||
}
|
||||
|
||||
return this.dataService.fetchPortfolioHoldings({
|
||||
filters,
|
||||
range: this.user?.settings?.dateRange
|
||||
});
|
||||
}
|
||||
|
||||
private openPositionDialog({
|
||||
dataSource,
|
||||
symbol
|
||||
}: {
|
||||
dataSource: DataSource;
|
||||
symbol: string;
|
||||
}) {
|
||||
this.userService
|
||||
.get()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((user) => {
|
||||
this.user = user;
|
||||
|
||||
const dialogRef = this.dialog.open(PositionDetailDialog, {
|
||||
autoFocus: false,
|
||||
data: <PositionDetailDialogParams>{
|
||||
dataSource,
|
||||
symbol,
|
||||
baseCurrency: this.user?.settings?.baseCurrency,
|
||||
colorScheme: this.user?.settings?.colorScheme,
|
||||
deviceType: this.deviceType,
|
||||
hasImpersonationId: this.hasImpersonationId,
|
||||
hasPermissionToReportDataGlitch: hasPermission(
|
||||
this.user?.permissions,
|
||||
permissions.reportDataGlitch
|
||||
),
|
||||
locale: this.user?.settings?.locale
|
||||
},
|
||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||
});
|
||||
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
this.router.navigate(['.'], { relativeTo: this.route });
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Holdings</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg">
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-toggle
|
||||
class="d-none d-lg-block"
|
||||
[defaultValue]="holdingType"
|
||||
[isLoading]="false"
|
||||
[options]="holdingTypeOptions"
|
||||
(change)="onChangeHoldingType($event.value)"
|
||||
/>
|
||||
</div>
|
||||
<gf-holdings-table
|
||||
[baseCurrency]="user?.settings?.baseCurrency"
|
||||
[deviceType]="deviceType"
|
||||
[hasPermissionToCreateActivity]="hasPermissionToCreateOrder"
|
||||
[holdings]="holdings"
|
||||
[locale]="user?.settings?.locale"
|
||||
/>
|
||||
@if (hasPermissionToCreateOrder && holdings?.length > 0) {
|
||||
<div class="text-center">
|
||||
<a
|
||||
class="mt-3"
|
||||
i18n
|
||||
mat-stroked-button
|
||||
[routerLink]="['/portfolio', 'activities']"
|
||||
>Manage Activities</a
|
||||
>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,22 +0,0 @@
|
||||
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
||||
import { GfHoldingsTableComponent } from '@ghostfolio/ui/holdings-table';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
|
||||
import { HoldingsPageRoutingModule } from './holdings-page-routing.module';
|
||||
import { HoldingsPageComponent } from './holdings-page.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [HoldingsPageComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfHoldingsTableComponent,
|
||||
GfToggleModule,
|
||||
HoldingsPageRoutingModule,
|
||||
MatButtonModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class HoldingsPageModule {}
|
@ -1,3 +0,0 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
@ -16,13 +16,6 @@ const routes: Routes = [
|
||||
(m) => m.AnalysisPageModule
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'holdings',
|
||||
loadChildren: () =>
|
||||
import('./holdings/holdings-page.module').then(
|
||||
(m) => m.HoldingsPageModule
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'activities',
|
||||
loadChildren: () =>
|
||||
|
@ -34,11 +34,6 @@ export class PortfolioPageComponent implements OnDestroy, OnInit {
|
||||
label: $localize`Analysis`,
|
||||
path: ['/portfolio']
|
||||
},
|
||||
{
|
||||
iconName: 'wallet-outline',
|
||||
label: $localize`Holdings`,
|
||||
path: ['/portfolio', 'holdings']
|
||||
},
|
||||
{
|
||||
iconName: 'swap-vertical-outline',
|
||||
label: $localize`Activities`,
|
||||
|
@ -6,7 +6,6 @@ import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
||||
import { Activities } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
||||
import { PortfolioPositionDetail } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position-detail.interface';
|
||||
import { PortfolioPositions } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-positions.interface';
|
||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||
import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface';
|
||||
import { UserItem } from '@ghostfolio/api/app/user/interfaces/user-item.interface';
|
||||
@ -376,21 +375,6 @@ export class DataService {
|
||||
});
|
||||
}
|
||||
|
||||
public fetchPositions({
|
||||
filters,
|
||||
range
|
||||
}: {
|
||||
filters?: Filter[];
|
||||
range: DateRange;
|
||||
}): Observable<PortfolioPositions> {
|
||||
let params = this.buildFiltersAsQueryParams({ filters });
|
||||
params = params.append('range', range);
|
||||
|
||||
return this.http.get<PortfolioPositions>('/api/v1/portfolio/positions', {
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
public fetchSymbols({
|
||||
includeIndices = false,
|
||||
query
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -387,6 +387,10 @@ ngx-skeleton-loader {
|
||||
@include gf-table;
|
||||
}
|
||||
|
||||
.has-fab {
|
||||
padding-bottom: 3rem !important;
|
||||
}
|
||||
|
||||
.has-info-message {
|
||||
.page.has-tabs {
|
||||
height: calc(100svh - 2 * var(--mat-toolbar-standard-height));
|
||||
@ -543,6 +547,10 @@ ngx-skeleton-loader {
|
||||
--mdc-tab-indicator-active-indicator-color: transparent;
|
||||
}
|
||||
|
||||
.mat-mdc-tab-nav-panel {
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
.mat-mdc-tab-link {
|
||||
--mdc-secondary-navigation-tab-container-height: 3rem;
|
||||
@ -567,10 +575,6 @@ ngx-skeleton-loader {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mat-mdc-tab-nav-panel {
|
||||
padding: 2rem 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ export const EMERGENCY_FUND_TAG_ID = '4452656d-9fa4-4bd0-ba38-70492e31d180';
|
||||
|
||||
export const GATHER_ASSET_PROFILE_PROCESS = 'GATHER_ASSET_PROFILE';
|
||||
export const GATHER_ASSET_PROFILE_PROCESS_OPTIONS: JobOptions = {
|
||||
attempts: 10,
|
||||
attempts: 12,
|
||||
backoff: {
|
||||
delay: ms('1 minute'),
|
||||
type: 'exponential'
|
||||
@ -76,7 +76,7 @@ export const GATHER_ASSET_PROFILE_PROCESS_OPTIONS: JobOptions = {
|
||||
export const GATHER_HISTORICAL_MARKET_DATA_PROCESS =
|
||||
'GATHER_HISTORICAL_MARKET_DATA';
|
||||
export const GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS: JobOptions = {
|
||||
attempts: 10,
|
||||
attempts: 12,
|
||||
backoff: {
|
||||
delay: ms('1 minute'),
|
||||
type: 'exponential'
|
||||
|
@ -1,14 +1,14 @@
|
||||
export interface PortfolioPerformance {
|
||||
annualizedPerformancePercent?: number;
|
||||
currentGrossPerformance: number;
|
||||
currentGrossPerformancePercent: number;
|
||||
currentGrossPerformancePercentWithCurrencyEffect: number;
|
||||
currentGrossPerformanceWithCurrencyEffect: number;
|
||||
currentNetPerformance: number;
|
||||
currentNetPerformancePercent: number;
|
||||
currentNetPerformancePercentWithCurrencyEffect: number;
|
||||
currentNetPerformanceWithCurrencyEffect: number;
|
||||
currentNetWorth: number;
|
||||
currentValue: number;
|
||||
currentNetWorth?: number;
|
||||
currentValueInBaseCurrency: number;
|
||||
grossPerformance: number;
|
||||
grossPerformancePercentage: number;
|
||||
grossPerformancePercentageWithCurrencyEffect: number;
|
||||
grossPerformanceWithCurrencyEffect: number;
|
||||
netPerformance: number;
|
||||
netPerformancePercentage: number;
|
||||
netPerformancePercentageWithCurrencyEffect: number;
|
||||
netPerformanceWithCurrencyEffect: number;
|
||||
totalInvestment: number;
|
||||
}
|
||||
|
@ -119,7 +119,7 @@
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="icon">
|
||||
<ng-container matColumnDef="icon" sticky>
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell></th>
|
||||
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
||||
<gf-asset-profile-icon
|
||||
|
@ -416,7 +416,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
|
||||
|
||||
private searchHoldings(aSearchTerm: string): Observable<ISearchResultItem[]> {
|
||||
return this.dataService
|
||||
.fetchPositions({
|
||||
.fetchPortfolioHoldings({
|
||||
filters: [
|
||||
{
|
||||
id: aSearchTerm,
|
||||
@ -429,8 +429,8 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
|
||||
catchError(() => {
|
||||
return EMPTY;
|
||||
}),
|
||||
map(({ positions }) => {
|
||||
return positions.map(
|
||||
map(({ holdings }) => {
|
||||
return holdings.map(
|
||||
({ assetSubClass, currency, dataSource, name, symbol }) => {
|
||||
return {
|
||||
currency,
|
||||
|
@ -7,7 +7,7 @@
|
||||
matSortDirection="desc"
|
||||
[dataSource]="dataSource"
|
||||
>
|
||||
<ng-container matColumnDef="icon">
|
||||
<ng-container matColumnDef="icon" sticky>
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell></th>
|
||||
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
||||
<gf-asset-profile-icon
|
||||
@ -109,7 +109,30 @@
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="performance" stickyEnd>
|
||||
<ng-container matColumnDef="performance">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="justify-content-end px-1"
|
||||
mat-header-cell
|
||||
mat-sort-header="netPerformanceWithCurrencyEffect"
|
||||
>
|
||||
<ng-container i18n>Change</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-value
|
||||
[colorizeSign]="true"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="
|
||||
isLoading ? undefined : element.netPerformanceWithCurrencyEffect
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="performanceInPercentage" stickyEnd>
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="justify-content-end px-1"
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { GfAssetProfileIconComponent } from '@ghostfolio/client/components/asset-profile-icon/asset-profile-icon.component';
|
||||
import { GfPositionDetailDialogModule } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.module';
|
||||
import { GfPositionDetailDialogModule } from '@ghostfolio/client/components/position-detail-dialog/position-detail-dialog.module';
|
||||
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||
import { getLocale } from '@ghostfolio/common/helper';
|
||||
import { PortfolioPosition, UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||
@ -84,7 +84,12 @@ export class GfHoldingsTableComponent implements OnChanges, OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
this.displayedColumns.push('allocationInPercentage');
|
||||
this.displayedColumns.push('performance');
|
||||
|
||||
if (this.hasPermissionToShowValues) {
|
||||
this.displayedColumns.push('performance');
|
||||
}
|
||||
|
||||
this.displayedColumns.push('performanceInPercentage');
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
|
@ -5,6 +5,7 @@ const locales = {
|
||||
'Asia-Pacific': $localize`Asia-Pacific`,
|
||||
ASSET_CLASS: $localize`Asset Class`,
|
||||
ASSET_SUB_CLASS: $localize`Asset Sub Class`,
|
||||
BUY_AND_SELL_ACTIVITIES_TOOLTIP: $localize`Buy and sell`,
|
||||
CORE: $localize`Core`,
|
||||
DATA_IMPORT_AND_EXPORT_TOOLTIP_BASIC: $localize`Switch to Ghostfolio Premium or Ghostfolio Open Source easily`,
|
||||
DATA_IMPORT_AND_EXPORT_TOOLTIP_OSS: $localize`Switch to Ghostfolio Premium easily`,
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div *ngIf="icon" class="align-self-center mr-3">
|
||||
<ion-icon class="h3 m-0" [name]="icon" />
|
||||
</div>
|
||||
<div class="w-100">
|
||||
<div class="d-flex flex-column w-100">
|
||||
<ng-template #label><ng-content></ng-content></ng-template>
|
||||
<ng-container *ngIf="value || value === 0 || value === null">
|
||||
<div
|
||||
@ -73,13 +73,13 @@
|
||||
/>
|
||||
|
||||
<ng-container>
|
||||
<div *ngIf="size === 'large'">
|
||||
<div *ngIf="size === 'large'" class="text-truncate">
|
||||
<span class="h6"
|
||||
><ng-container *ngTemplateOutlet="label"></ng-container
|
||||
></span>
|
||||
<span *ngIf="subLabel" class="text-muted"> {{ subLabel }}</span>
|
||||
</div>
|
||||
<small *ngIf="size !== 'large'">
|
||||
<small *ngIf="size !== 'large'" class="d-block text-truncate">
|
||||
<ng-container *ngTemplateOutlet="label"></ng-container>
|
||||
</small>
|
||||
</ng-container>
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ghostfolio",
|
||||
"version": "2.78.0",
|
||||
"version": "2.80.0",
|
||||
"homepage": "https://ghostfol.io",
|
||||
"license": "AGPL-3.0",
|
||||
"repository": "https://github.com/ghostfolio/ghostfolio",
|
||||
@ -112,7 +112,7 @@
|
||||
"got": "11.8.6",
|
||||
"helmet": "7.0.0",
|
||||
"http-status-codes": "2.3.0",
|
||||
"ionicons": "7.3.0",
|
||||
"ionicons": "7.4.0",
|
||||
"jsonpath": "1.1.1",
|
||||
"lodash": "4.17.21",
|
||||
"marked": "9.1.6",
|
||||
|
@ -13104,10 +13104,10 @@ invariant@^2.2.4:
|
||||
dependencies:
|
||||
loose-envify "^1.0.0"
|
||||
|
||||
ionicons@7.3.0:
|
||||
version "7.3.0"
|
||||
resolved "https://registry.yarnpkg.com/ionicons/-/ionicons-7.3.0.tgz#d2385e87dbe6a8c79fd44fb2e7c87b0de4a2cbcb"
|
||||
integrity sha512-l9quySYi+o4T6mFzhKRyU/1nKc2Zs0zxs7jWcq9iVRhRPQondV11jYqLTed0lVVXHfGrBCfnedKl9D6BCnA1UQ==
|
||||
ionicons@7.4.0:
|
||||
version "7.4.0"
|
||||
resolved "https://registry.yarnpkg.com/ionicons/-/ionicons-7.4.0.tgz#9c285aaa8089befbd6c5a89ae13292d364cd9ace"
|
||||
integrity sha512-ZK94MMqgzMCPPMhmk8Ouu6goyVHFIlw/ACP6oe3FrikcI0N7CX0xcwVaEbUc0G/v3W0shI93vo+9ve/KpvcNhQ==
|
||||
dependencies:
|
||||
"@stencil/core" "^4.0.3"
|
||||
|
||||
|
Reference in New Issue
Block a user