Compare commits
32 Commits
Author | SHA1 | Date | |
---|---|---|---|
dc9b2ce194 | |||
72067459d6 | |||
705441ecf8 | |||
fbd1475402 | |||
4dc4f13f40 | |||
3b857aa8bb | |||
1c2ca5b96b | |||
572bfc59b8 | |||
147f0162b7 | |||
f6acf5207b | |||
80782f1098 | |||
bc58ee86ca | |||
0cb632b165 | |||
fca3a659d0 | |||
904dec040e | |||
fc6c81fe02 | |||
634171e4e3 | |||
f8f36e4f4e | |||
5e7cf9d0b6 | |||
e1932eb5a1 | |||
dba47d59e3 | |||
3032126508 | |||
a50b55da75 | |||
5422df05b3 | |||
d2fabe7ce4 | |||
a42700b9fe | |||
9df8541145 | |||
0b2252755c | |||
239bd09cbd | |||
cd76f89902 | |||
7425ba94f1 | |||
b9522307c4 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -23,6 +23,7 @@
|
||||
!.vscode/settings.json
|
||||
|
||||
# misc
|
||||
/.angular/cache
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
|
94
CHANGELOG.md
94
CHANGELOG.md
@ -5,6 +5,100 @@ 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).
|
||||
|
||||
## 1.81.0 - 27.11.2021
|
||||
|
||||
### Added
|
||||
|
||||
- Added the value to the position detail dialog
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgraded `angular` from version `12.2.4` to `13.0.2`
|
||||
- Upgraded `angular-material-css-vars` from version `2.1.2` to `3.0.0`
|
||||
- Upgraded `nestjs` from version `7.6.18` to `8.2.3`
|
||||
- Upgraded `Nx` from version `12.8.0` to `13.2.2`
|
||||
- Upgraded `rxjs` from version `6.6.7` to `7.4.0`
|
||||
- Upgraded `storybook` from version `6.3.8` to `6.4.0-rc.3`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the broken line charts showing value labels if openend from the allocations page
|
||||
- Fixed the click event for drafts in the transactions table
|
||||
|
||||
## 1.80.0 - 23.11.2021
|
||||
|
||||
### Added
|
||||
|
||||
- Accentuated the all time high and the all time low
|
||||
|
||||
## 1.79.0 - 21.11.2021
|
||||
|
||||
### Added
|
||||
|
||||
- Added the value column to the positions table
|
||||
- Added support for cryptocurrency _Algorand_
|
||||
|
||||
### Changed
|
||||
|
||||
- Locked the symbol input in the edit transaction dialog
|
||||
- Filtered the account selector by account type (`SECURITIES`) in the create or edit transaction dialog
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the search functionality for cryptocurrency symbols (do not show unsupported symbols)
|
||||
|
||||
## 1.78.0 - 20.11.2021
|
||||
|
||||
### Added
|
||||
|
||||
- Added a testimonial section to the landing page
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the footer row border of the accounts table in dark mode
|
||||
|
||||
## 1.77.0 - 16.11.2021
|
||||
|
||||
### Changed
|
||||
|
||||
- Hid the _Get Started_ button on the registration page
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the footer row of the accounts table on mobile
|
||||
- Fixed the transactions count calculation in the accounts table (exclude drafts)
|
||||
|
||||
## 1.76.0 - 14.11.2021
|
||||
|
||||
### Added
|
||||
|
||||
- Added the footer row with buying power and net worth to the accounts table
|
||||
|
||||
## 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
|
||||
|
||||
### Changed
|
||||
|
37
angular.json
37
angular.json
@ -1,22 +1,5 @@
|
||||
{
|
||||
"version": 1,
|
||||
"cli": {
|
||||
"defaultCollection": "@nrwl/nest"
|
||||
},
|
||||
"defaultProject": "api",
|
||||
"schematics": {
|
||||
"@nrwl/angular:application": {
|
||||
"linter": "eslint",
|
||||
"unitTestRunner": "jest",
|
||||
"e2eTestRunner": "cypress"
|
||||
},
|
||||
"@nrwl/angular:library": {
|
||||
"linter": "eslint",
|
||||
"unitTestRunner": "jest"
|
||||
},
|
||||
"@nrwl/nest": {},
|
||||
"@nrwl/angular:component": {}
|
||||
},
|
||||
"projects": {
|
||||
"api": {
|
||||
"root": "apps/api",
|
||||
@ -69,7 +52,8 @@
|
||||
},
|
||||
"outputs": ["coverage/apps/api"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": []
|
||||
},
|
||||
"client": {
|
||||
"projectType": "application",
|
||||
@ -201,7 +185,8 @@
|
||||
},
|
||||
"outputs": ["coverage/apps/client"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": []
|
||||
},
|
||||
"client-e2e": {
|
||||
"root": "apps/client-e2e",
|
||||
@ -221,7 +206,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [],
|
||||
"implicitDependencies": ["client"]
|
||||
},
|
||||
"common": {
|
||||
"root": "libs/common",
|
||||
@ -242,7 +229,8 @@
|
||||
"passWithNoTests": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": []
|
||||
},
|
||||
"ui": {
|
||||
"projectType": "library",
|
||||
@ -300,7 +288,8 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": []
|
||||
},
|
||||
"ui-e2e": {
|
||||
"root": "apps/ui-e2e",
|
||||
@ -326,7 +315,9 @@
|
||||
"lintFilePatterns": ["apps/ui-e2e/**/*.{js,ts}"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [],
|
||||
"implicitDependencies": ["ui"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,11 @@
|
||||
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
|
||||
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||
import { nullifyValuesInObjects } from '@ghostfolio/api/helper/object.helper';
|
||||
import {
|
||||
nullifyValuesInObject,
|
||||
nullifyValuesInObjects
|
||||
} from '@ghostfolio/api/helper/object.helper';
|
||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
||||
import { Accounts } from '@ghostfolio/common/interfaces';
|
||||
import {
|
||||
getPermissions,
|
||||
hasPermission,
|
||||
@ -34,6 +39,7 @@ export class AccountController {
|
||||
public constructor(
|
||||
private readonly accountService: AccountService,
|
||||
private readonly impersonationService: ImpersonationService,
|
||||
private readonly portfolioService: PortfolioService,
|
||||
@Inject(REQUEST) private readonly request: RequestWithUser,
|
||||
private readonly userService: UserService
|
||||
) {}
|
||||
@ -85,30 +91,39 @@ export class AccountController {
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async getAllAccounts(
|
||||
@Headers('impersonation-id') impersonationId
|
||||
): Promise<AccountModel[]> {
|
||||
): Promise<Accounts> {
|
||||
const impersonationUserId =
|
||||
await this.impersonationService.validateImpersonationId(
|
||||
impersonationId,
|
||||
this.request.user.id
|
||||
);
|
||||
|
||||
let accounts = await this.accountService.getAccounts(
|
||||
impersonationUserId || this.request.user.id
|
||||
);
|
||||
let accountsWithAggregations =
|
||||
await this.portfolioService.getAccountsWithAggregations(
|
||||
impersonationUserId || this.request.user.id
|
||||
);
|
||||
|
||||
if (
|
||||
impersonationUserId ||
|
||||
this.userService.isRestrictedView(this.request.user)
|
||||
) {
|
||||
accounts = nullifyValuesInObjects(accounts, [
|
||||
'balance',
|
||||
'fee',
|
||||
'quantity',
|
||||
'unitPrice'
|
||||
]);
|
||||
accountsWithAggregations = {
|
||||
...nullifyValuesInObject(accountsWithAggregations, [
|
||||
'totalBalance',
|
||||
'totalValue'
|
||||
]),
|
||||
accounts: nullifyValuesInObjects(accountsWithAggregations.accounts, [
|
||||
'balance',
|
||||
'convertedBalance',
|
||||
'fee',
|
||||
'quantity',
|
||||
'unitPrice',
|
||||
'value'
|
||||
])
|
||||
};
|
||||
}
|
||||
|
||||
return accounts;
|
||||
return accountsWithAggregations;
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
|
@ -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 { UserModule } from '@ghostfolio/api/app/user/user.module';
|
||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||
@ -11,16 +12,17 @@ import { AccountController } from './account.controller';
|
||||
import { AccountService } from './account.service';
|
||||
|
||||
@Module({
|
||||
controllers: [AccountController],
|
||||
imports: [
|
||||
ConfigurationModule,
|
||||
DataProviderModule,
|
||||
ExchangeRateDataModule,
|
||||
ImpersonationModule,
|
||||
RedisCacheModule,
|
||||
PortfolioModule,
|
||||
PrismaModule,
|
||||
RedisCacheModule,
|
||||
UserModule
|
||||
],
|
||||
controllers: [AccountController],
|
||||
providers: [AccountService]
|
||||
})
|
||||
export class AccountModule {}
|
||||
|
@ -85,7 +85,15 @@ export class AccountService {
|
||||
});
|
||||
|
||||
return accounts.map((account) => {
|
||||
const result = { ...account, transactionCount: account.Order.length };
|
||||
let transactionCount = 0;
|
||||
|
||||
for (const order of account.Order) {
|
||||
if (!order.isDraft) {
|
||||
transactionCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const result = { ...account, transactionCount };
|
||||
|
||||
delete result.Order;
|
||||
|
||||
|
@ -20,6 +20,8 @@ export class AdminService {
|
||||
|
||||
public async get(): Promise<AdminData> {
|
||||
return {
|
||||
dataGatheringProgress:
|
||||
await this.dataGatheringService.getDataGatheringProgress(),
|
||||
exchangeRates: this.exchangeRateDataService
|
||||
.getCurrencies()
|
||||
.filter((currency) => {
|
||||
@ -58,7 +60,7 @@ export class AdminService {
|
||||
return 'IN_PROGRESS';
|
||||
}
|
||||
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async getUsersWithAnalytics(): Promise<AdminData['users']> {
|
||||
|
@ -1,4 +1,8 @@
|
||||
import { AssetClass, AssetSubClass } from '@prisma/client';
|
||||
|
||||
export interface PortfolioPositionDetail {
|
||||
assetClass?: AssetClass;
|
||||
assetSubClass?: AssetSubClass;
|
||||
averagePrice: number;
|
||||
currency: string;
|
||||
firstBuyDate: string;
|
||||
@ -15,6 +19,13 @@ export interface PortfolioPositionDetail {
|
||||
quantity: number;
|
||||
symbol: string;
|
||||
transactionCount: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface HistoricalDataContainer {
|
||||
isAllTimeHigh: boolean;
|
||||
isAllTimeLow: boolean;
|
||||
items: HistoricalDataItem[];
|
||||
}
|
||||
|
||||
export interface HistoricalDataItem {
|
||||
|
@ -0,0 +1,8 @@
|
||||
import { TimelinePeriod } from '@ghostfolio/api/app/portfolio/interfaces/timeline-period.interface';
|
||||
import Big from 'big.js';
|
||||
|
||||
export interface TimelineInfoInterface {
|
||||
maxNetPerformance: Big;
|
||||
minNetPerformance: Big;
|
||||
timelinePeriods: TimelinePeriod[];
|
||||
}
|
@ -1502,11 +1502,11 @@ describe('PortfolioCalculator', () => {
|
||||
accuracy: 'year'
|
||||
}
|
||||
];
|
||||
const timeline: TimelinePeriod[] =
|
||||
await portfolioCalculator.calculateTimeline(
|
||||
timelineSpecification,
|
||||
'2021-06-30'
|
||||
);
|
||||
const timelineInfo = await portfolioCalculator.calculateTimeline(
|
||||
timelineSpecification,
|
||||
'2021-06-30'
|
||||
);
|
||||
const timeline: TimelinePeriod[] = timelineInfo.timelinePeriods;
|
||||
|
||||
expect(timeline).toEqual([
|
||||
{
|
||||
@ -1622,11 +1622,11 @@ describe('PortfolioCalculator', () => {
|
||||
accuracy: 'year'
|
||||
}
|
||||
];
|
||||
const timeline: TimelinePeriod[] =
|
||||
await portfolioCalculator.calculateTimeline(
|
||||
timelineSpecification,
|
||||
'2021-06-30'
|
||||
);
|
||||
const timelineInfo = await portfolioCalculator.calculateTimeline(
|
||||
timelineSpecification,
|
||||
'2021-06-30'
|
||||
);
|
||||
const timeline: TimelinePeriod[] = timelineInfo.timelinePeriods;
|
||||
|
||||
expect(timeline).toEqual([
|
||||
{
|
||||
@ -1665,11 +1665,11 @@ describe('PortfolioCalculator', () => {
|
||||
accuracy: 'month'
|
||||
}
|
||||
];
|
||||
const timeline: TimelinePeriod[] =
|
||||
await portfolioCalculator.calculateTimeline(
|
||||
timelineSpecification,
|
||||
'2021-06-30'
|
||||
);
|
||||
const timelineInfo = await portfolioCalculator.calculateTimeline(
|
||||
timelineSpecification,
|
||||
'2021-06-30'
|
||||
);
|
||||
const timeline: TimelinePeriod[] = timelineInfo.timelinePeriods;
|
||||
|
||||
expect(timeline).toEqual([
|
||||
{
|
||||
@ -1883,6 +1883,9 @@ describe('PortfolioCalculator', () => {
|
||||
value: new Big('3186.9') // 15 * (144.38 + days=851 * 0.08)
|
||||
}
|
||||
]);
|
||||
|
||||
expect(timelineInfo.maxNetPerformance).toEqual(new Big('547.9'));
|
||||
expect(timelineInfo.minNetPerformance).toEqual(new Big('0'));
|
||||
});
|
||||
|
||||
it('with yearly and monthly mixed', async () => {
|
||||
@ -1901,11 +1904,11 @@ describe('PortfolioCalculator', () => {
|
||||
accuracy: 'month'
|
||||
}
|
||||
];
|
||||
const timeline: TimelinePeriod[] =
|
||||
await portfolioCalculator.calculateTimeline(
|
||||
timelineSpecification,
|
||||
'2021-06-30'
|
||||
);
|
||||
const timelineInfo = await portfolioCalculator.calculateTimeline(
|
||||
timelineSpecification,
|
||||
'2021-06-30'
|
||||
);
|
||||
const timeline: TimelinePeriod[] = timelineInfo.timelinePeriods;
|
||||
|
||||
expect(timeline).toEqual([
|
||||
{
|
||||
@ -1987,11 +1990,11 @@ describe('PortfolioCalculator', () => {
|
||||
accuracy: 'day'
|
||||
}
|
||||
];
|
||||
const timeline: TimelinePeriod[] =
|
||||
await portfolioCalculator.calculateTimeline(
|
||||
timelineSpecification,
|
||||
'2021-06-30'
|
||||
);
|
||||
const timelineInfo = await portfolioCalculator.calculateTimeline(
|
||||
timelineSpecification,
|
||||
'2021-06-30'
|
||||
);
|
||||
const timeline: TimelinePeriod[] = timelineInfo.timelinePeriods;
|
||||
|
||||
expect(timeline).toEqual(
|
||||
expect.objectContaining([
|
||||
@ -2296,11 +2299,11 @@ describe('PortfolioCalculator', () => {
|
||||
accuracy: 'year'
|
||||
}
|
||||
];
|
||||
const timeline: TimelinePeriod[] =
|
||||
await portfolioCalculator.calculateTimeline(
|
||||
timelineSpecification,
|
||||
'2020-01-01'
|
||||
);
|
||||
const timelineInfo = await portfolioCalculator.calculateTimeline(
|
||||
timelineSpecification,
|
||||
'2020-01-01'
|
||||
);
|
||||
const timeline: TimelinePeriod[] = timelineInfo.timelinePeriods;
|
||||
|
||||
expect(timeline).toEqual([
|
||||
{
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { TimelineInfoInterface } from '@ghostfolio/api/app/portfolio/interfaces/timeline-info.interface';
|
||||
import { OrderType } from '@ghostfolio/api/models/order-type';
|
||||
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
|
||||
@ -365,16 +366,20 @@ export class PortfolioCalculator {
|
||||
public async calculateTimeline(
|
||||
timelineSpecification: TimelineSpecification[],
|
||||
endDate: string
|
||||
): Promise<TimelinePeriod[]> {
|
||||
): Promise<TimelineInfoInterface> {
|
||||
if (timelineSpecification.length === 0) {
|
||||
return [];
|
||||
return {
|
||||
maxNetPerformance: new Big(0),
|
||||
minNetPerformance: new Big(0),
|
||||
timelinePeriods: []
|
||||
};
|
||||
}
|
||||
|
||||
const startDate = timelineSpecification[0].start;
|
||||
const start = parseDate(startDate);
|
||||
const end = parseDate(endDate);
|
||||
|
||||
const timelinePeriodPromises: Promise<TimelinePeriod[]>[] = [];
|
||||
const timelinePeriodPromises: Promise<TimelineInfoInterface>[] = [];
|
||||
let i = 0;
|
||||
let j = -1;
|
||||
for (
|
||||
@ -417,11 +422,40 @@ export class PortfolioCalculator {
|
||||
}
|
||||
}
|
||||
|
||||
const timelinePeriods: TimelinePeriod[][] = await Promise.all(
|
||||
const timelineInfoInterfaces: TimelineInfoInterface[] = await Promise.all(
|
||||
timelinePeriodPromises
|
||||
);
|
||||
const minNetPerformance = timelineInfoInterfaces
|
||||
.map((timelineInfo) => timelineInfo.minNetPerformance)
|
||||
.filter((performance) => performance !== null)
|
||||
.reduce((minPerformance, current) => {
|
||||
if (minPerformance.lt(current)) {
|
||||
return minPerformance;
|
||||
} else {
|
||||
return current;
|
||||
}
|
||||
});
|
||||
|
||||
return flatten(timelinePeriods);
|
||||
const maxNetPerformance = timelineInfoInterfaces
|
||||
.map((timelineInfo) => timelineInfo.maxNetPerformance)
|
||||
.filter((performance) => performance !== null)
|
||||
.reduce((maxPerformance, current) => {
|
||||
if (maxPerformance.gt(current)) {
|
||||
return maxPerformance;
|
||||
} else {
|
||||
return current;
|
||||
}
|
||||
});
|
||||
|
||||
const timelinePeriods = timelineInfoInterfaces.map(
|
||||
(timelineInfo) => timelineInfo.timelinePeriods
|
||||
);
|
||||
|
||||
return {
|
||||
maxNetPerformance,
|
||||
minNetPerformance,
|
||||
timelinePeriods: flatten(timelinePeriods)
|
||||
};
|
||||
}
|
||||
|
||||
private calculateOverallPerformance(
|
||||
@ -513,7 +547,7 @@ export class PortfolioCalculator {
|
||||
j: number,
|
||||
startDate: Date,
|
||||
endDate: Date
|
||||
): Promise<TimelinePeriod[]> {
|
||||
): Promise<TimelineInfoInterface> {
|
||||
let investment: Big = new Big(0);
|
||||
let fees: Big = new Big(0);
|
||||
|
||||
@ -569,6 +603,8 @@ export class PortfolioCalculator {
|
||||
}
|
||||
|
||||
const results: TimelinePeriod[] = [];
|
||||
let maxNetPerformance: Big = null;
|
||||
let minNetPerformance: Big = null;
|
||||
for (
|
||||
let currentDate = startDate;
|
||||
isBefore(currentDate, endDate);
|
||||
@ -592,18 +628,36 @@ export class PortfolioCalculator {
|
||||
}
|
||||
if (!invalid) {
|
||||
const grossPerformance = value.minus(investment);
|
||||
const netPerformance = grossPerformance.minus(fees);
|
||||
if (
|
||||
minNetPerformance === null ||
|
||||
minNetPerformance.gt(netPerformance)
|
||||
) {
|
||||
minNetPerformance = netPerformance;
|
||||
}
|
||||
if (
|
||||
maxNetPerformance === null ||
|
||||
maxNetPerformance.lt(netPerformance)
|
||||
) {
|
||||
maxNetPerformance = netPerformance;
|
||||
}
|
||||
|
||||
const result = {
|
||||
grossPerformance,
|
||||
investment,
|
||||
netPerformance,
|
||||
value,
|
||||
date: currentDateAsString,
|
||||
netPerformance: grossPerformance.minus(fees)
|
||||
date: currentDateAsString
|
||||
};
|
||||
results.push(result);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
return {
|
||||
maxNetPerformance,
|
||||
minNetPerformance,
|
||||
timelinePeriods: results
|
||||
};
|
||||
}
|
||||
|
||||
private getFactor(type: OrderType) {
|
||||
|
@ -8,6 +8,7 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration.ser
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||
import { baseCurrency } from '@ghostfolio/common/config';
|
||||
import {
|
||||
PortfolioChart,
|
||||
PortfolioDetails,
|
||||
PortfolioPerformance,
|
||||
PortfolioPublicDetails,
|
||||
@ -32,10 +33,7 @@ import { AuthGuard } from '@nestjs/passport';
|
||||
import { Response } from 'express';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
|
||||
import {
|
||||
HistoricalDataItem,
|
||||
PortfolioPositionDetail
|
||||
} from './interfaces/portfolio-position-detail.interface';
|
||||
import { PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface';
|
||||
import { PortfolioPositions } from './interfaces/portfolio-positions.interface';
|
||||
import { PortfolioService } from './portfolio.service';
|
||||
|
||||
@ -92,12 +90,14 @@ export class PortfolioController {
|
||||
@Headers('impersonation-id') impersonationId,
|
||||
@Query('range') range,
|
||||
@Res() res: Response
|
||||
): Promise<HistoricalDataItem[]> {
|
||||
let chartData = await this.portfolioService.getChart(
|
||||
): Promise<PortfolioChart> {
|
||||
const historicalDataContainer = await this.portfolioService.getChart(
|
||||
impersonationId,
|
||||
range
|
||||
);
|
||||
|
||||
let chartData = historicalDataContainer.items;
|
||||
|
||||
let hasNullValue = false;
|
||||
|
||||
chartData.forEach((chartDataItem) => {
|
||||
@ -130,7 +130,11 @@ export class PortfolioController {
|
||||
});
|
||||
}
|
||||
|
||||
return <any>res.json(chartData);
|
||||
return <any>res.json({
|
||||
chart: chartData,
|
||||
isAllTimeHigh: historicalDataContainer.isAllTimeHigh,
|
||||
isAllTimeLow: historicalDataContainer.isAllTimeLow
|
||||
});
|
||||
}
|
||||
|
||||
@Get('details')
|
||||
@ -366,7 +370,8 @@ export class PortfolioController {
|
||||
'grossPerformance',
|
||||
'investment',
|
||||
'netPerformance',
|
||||
'quantity'
|
||||
'quantity',
|
||||
'value'
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,7 @@ import { PortfolioService } from './portfolio.service';
|
||||
import { RulesService } from './rules.service';
|
||||
|
||||
@Module({
|
||||
exports: [PortfolioService],
|
||||
imports: [
|
||||
AccessModule,
|
||||
ConfigurationModule,
|
||||
|
@ -1,5 +1,3 @@
|
||||
// TODO ///////////
|
||||
|
||||
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||
import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details.interface';
|
||||
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
||||
@ -30,6 +28,7 @@ import {
|
||||
} from '@ghostfolio/common/config';
|
||||
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
Accounts,
|
||||
PortfolioDetails,
|
||||
PortfolioPerformance,
|
||||
PortfolioReport,
|
||||
@ -39,6 +38,7 @@ import {
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||
import type {
|
||||
AccountWithValue,
|
||||
DateRange,
|
||||
OrderWithAccount,
|
||||
RequestWithUser
|
||||
@ -56,12 +56,14 @@ import {
|
||||
parse,
|
||||
parseISO,
|
||||
setDayOfYear,
|
||||
startOfDay,
|
||||
subDays,
|
||||
subYears
|
||||
} from 'date-fns';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
import {
|
||||
HistoricalDataContainer,
|
||||
HistoricalDataItem,
|
||||
PortfolioPositionDetail
|
||||
} from './interfaces/portfolio-position-detail.interface';
|
||||
@ -81,6 +83,59 @@ export class PortfolioService {
|
||||
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) => {
|
||||
let transactionCount = 0;
|
||||
|
||||
for (const order of account.Order) {
|
||||
if (!order.isDraft) {
|
||||
transactionCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const result = {
|
||||
...account,
|
||||
transactionCount,
|
||||
convertedBalance: this.exchangeRateDataService.toCurrency(
|
||||
account.balance,
|
||||
account.currency,
|
||||
userCurrency
|
||||
),
|
||||
value: details.accounts[account.name]?.current ?? 0
|
||||
};
|
||||
|
||||
delete result.Order;
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
public async getAccountsWithAggregations(aUserId: string): Promise<Accounts> {
|
||||
const accounts = await this.getAccounts(aUserId);
|
||||
let totalBalance = 0;
|
||||
let totalValue = 0;
|
||||
let transactionCount = 0;
|
||||
|
||||
for (const account of accounts) {
|
||||
totalBalance += account.convertedBalance;
|
||||
totalValue += account.value;
|
||||
transactionCount += account.transactionCount;
|
||||
}
|
||||
|
||||
return { accounts, totalBalance, totalValue, transactionCount };
|
||||
}
|
||||
|
||||
public async getInvestments(
|
||||
aImpersonationId: string
|
||||
): Promise<InvestmentItem[]> {
|
||||
@ -111,7 +166,7 @@ export class PortfolioService {
|
||||
public async getChart(
|
||||
aImpersonationId: string,
|
||||
aDateRange: DateRange = 'max'
|
||||
): Promise<HistoricalDataItem[]> {
|
||||
): Promise<HistoricalDataContainer> {
|
||||
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
||||
|
||||
const portfolioCalculator = new PortfolioCalculator(
|
||||
@ -122,14 +177,21 @@ export class PortfolioService {
|
||||
const { transactionPoints } = await this.getTransactionPoints({ userId });
|
||||
portfolioCalculator.setTransactionPoints(transactionPoints);
|
||||
if (transactionPoints.length === 0) {
|
||||
return [];
|
||||
return {
|
||||
isAllTimeHigh: false,
|
||||
isAllTimeLow: false,
|
||||
items: []
|
||||
};
|
||||
}
|
||||
let portfolioStart = parse(
|
||||
transactionPoints[0].date,
|
||||
DATE_FORMAT,
|
||||
new Date()
|
||||
);
|
||||
portfolioStart = this.getStartDate(aDateRange, portfolioStart);
|
||||
|
||||
// Get start date for the full portfolio because of because of the
|
||||
// min and max calculation
|
||||
portfolioStart = this.getStartDate('max', portfolioStart);
|
||||
|
||||
const timelineSpecification: TimelineSpecification[] = [
|
||||
{
|
||||
@ -138,18 +200,52 @@ export class PortfolioService {
|
||||
}
|
||||
];
|
||||
|
||||
const timeline = await portfolioCalculator.calculateTimeline(
|
||||
const timelineInfo = await portfolioCalculator.calculateTimeline(
|
||||
timelineSpecification,
|
||||
format(new Date(), DATE_FORMAT)
|
||||
);
|
||||
|
||||
return timeline
|
||||
const timeline = timelineInfo.timelinePeriods;
|
||||
|
||||
const items = timeline
|
||||
.filter((timelineItem) => timelineItem !== null)
|
||||
.map((timelineItem) => ({
|
||||
date: timelineItem.date,
|
||||
marketPrice: timelineItem.value,
|
||||
value: timelineItem.netPerformance.toNumber()
|
||||
}));
|
||||
|
||||
let lastItem = null;
|
||||
if (timeline.length > 0) {
|
||||
lastItem = timeline[timeline.length - 1];
|
||||
}
|
||||
|
||||
let isAllTimeHigh = timelineInfo.maxNetPerformance?.eq(
|
||||
lastItem?.netPerformance
|
||||
);
|
||||
let isAllTimeLow = timelineInfo.minNetPerformance?.eq(
|
||||
lastItem?.netPerformance
|
||||
);
|
||||
if (isAllTimeHigh && isAllTimeLow) {
|
||||
isAllTimeHigh = false;
|
||||
isAllTimeLow = false;
|
||||
}
|
||||
|
||||
portfolioStart = startOfDay(
|
||||
this.getStartDate(
|
||||
aDateRange,
|
||||
parse(transactionPoints[0].date, DATE_FORMAT, new Date())
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
isAllTimeHigh,
|
||||
isAllTimeLow,
|
||||
items: items.filter((item) => {
|
||||
// Filter items of date range
|
||||
return !isAfter(portfolioStart, parseDate(item.date));
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
public async getDetails(
|
||||
@ -258,7 +354,7 @@ export class PortfolioService {
|
||||
value: totalValue
|
||||
});
|
||||
|
||||
const accounts = await this.getAccounts(
|
||||
const accounts = await this.getValueOfAccounts(
|
||||
orders,
|
||||
portfolioItemsNow,
|
||||
userCurrency,
|
||||
@ -295,10 +391,13 @@ export class PortfolioService {
|
||||
netPerformancePercent: undefined,
|
||||
quantity: undefined,
|
||||
symbol: aSymbol,
|
||||
transactionCount: undefined
|
||||
transactionCount: undefined,
|
||||
value: undefined
|
||||
};
|
||||
}
|
||||
|
||||
const assetClass = orders[0].SymbolProfile?.assetClass;
|
||||
const assetSubClass = orders[0].SymbolProfile?.assetSubClass;
|
||||
const positionCurrency = orders[0].currency;
|
||||
const name = orders[0].SymbolProfile?.name ?? '';
|
||||
|
||||
@ -412,6 +511,8 @@ export class PortfolioService {
|
||||
}
|
||||
|
||||
return {
|
||||
assetClass,
|
||||
assetSubClass,
|
||||
currency,
|
||||
firstBuyDate,
|
||||
grossPerformance,
|
||||
@ -427,7 +528,12 @@ export class PortfolioService {
|
||||
historicalData: historicalDataArray,
|
||||
netPerformancePercent: position.netPerformancePercentage.toNumber(),
|
||||
quantity: quantity.toNumber(),
|
||||
symbol: aSymbol
|
||||
symbol: aSymbol,
|
||||
value: this.exchangeRateDataService.toCurrency(
|
||||
quantity.mul(marketPrice).toNumber(),
|
||||
currency,
|
||||
userCurrency
|
||||
)
|
||||
};
|
||||
} else {
|
||||
const currentData = await this.dataProviderService.get([
|
||||
@ -467,6 +573,8 @@ export class PortfolioService {
|
||||
}
|
||||
|
||||
return {
|
||||
assetClass,
|
||||
assetSubClass,
|
||||
marketPrice,
|
||||
maxPrice,
|
||||
minPrice,
|
||||
@ -482,7 +590,8 @@ export class PortfolioService {
|
||||
netPerformancePercent: undefined,
|
||||
quantity: 0,
|
||||
symbol: aSymbol,
|
||||
transactionCount: undefined
|
||||
transactionCount: undefined,
|
||||
value: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -580,7 +689,9 @@ export class PortfolioService {
|
||||
currentGrossPerformancePercent: 0,
|
||||
currentNetPerformance: 0,
|
||||
currentNetPerformancePercent: 0,
|
||||
currentValue: 0
|
||||
currentValue: 0,
|
||||
isAllTimeHigh: false,
|
||||
isAllTimeLow: false
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -613,7 +724,9 @@ export class PortfolioService {
|
||||
currentGrossPerformancePercent,
|
||||
currentNetPerformance,
|
||||
currentNetPerformancePercent,
|
||||
currentValue: currentValue
|
||||
currentValue,
|
||||
isAllTimeHigh: true, // TODO
|
||||
isAllTimeLow: false // TODO
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -663,7 +776,7 @@ export class PortfolioService {
|
||||
for (const position of currentPositions.positions) {
|
||||
portfolioItemsNow[position.symbol] = position;
|
||||
}
|
||||
const accounts = await this.getAccounts(
|
||||
const accounts = await this.getValueOfAccounts(
|
||||
orders,
|
||||
portfolioItemsNow,
|
||||
currency,
|
||||
@ -863,7 +976,7 @@ export class PortfolioService {
|
||||
};
|
||||
}
|
||||
|
||||
private async getAccounts(
|
||||
private async getValueOfAccounts(
|
||||
orders: OrderWithAccount[],
|
||||
portfolioItemsNow: { [p: string]: TimelinePosition },
|
||||
userCurrency: string,
|
||||
@ -878,32 +991,20 @@ export class PortfolioService {
|
||||
return accountId === account.id;
|
||||
});
|
||||
|
||||
if (ordersByAccount.length <= 0) {
|
||||
// Add account without orders
|
||||
const balance = this.exchangeRateDataService.toCurrency(
|
||||
account.balance,
|
||||
account.currency,
|
||||
userCurrency
|
||||
);
|
||||
accounts[account.name] = {
|
||||
current: balance,
|
||||
original: balance
|
||||
};
|
||||
|
||||
continue;
|
||||
}
|
||||
const convertedBalance = this.exchangeRateDataService.toCurrency(
|
||||
account.balance,
|
||||
account.currency,
|
||||
userCurrency
|
||||
);
|
||||
accounts[account.name] = {
|
||||
current: convertedBalance,
|
||||
original: convertedBalance
|
||||
};
|
||||
|
||||
for (const order of ordersByAccount) {
|
||||
let currentValueOfSymbol = this.exchangeRateDataService.toCurrency(
|
||||
order.quantity * portfolioItemsNow[order.symbol].marketPrice,
|
||||
order.currency,
|
||||
userCurrency
|
||||
);
|
||||
let originalValueOfSymbol = this.exchangeRateDataService.toCurrency(
|
||||
order.quantity * order.unitPrice,
|
||||
order.currency,
|
||||
userCurrency
|
||||
);
|
||||
let currentValueOfSymbol =
|
||||
order.quantity * portfolioItemsNow[order.symbol].marketPrice;
|
||||
let originalValueOfSymbol = order.quantity * order.unitPrice;
|
||||
|
||||
if (order.type === 'SELL') {
|
||||
currentValueOfSymbol *= -1;
|
||||
|
@ -2,6 +2,7 @@ import { Logger, ValidationPipe } from '@nestjs/common';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
|
||||
import { AppModule } from './app/app.module';
|
||||
import { environment } from './environments/environment';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
@ -18,8 +19,21 @@ async function bootstrap() {
|
||||
|
||||
const port = process.env.PORT || 3333;
|
||||
await app.listen(port, () => {
|
||||
logLogo();
|
||||
Logger.log(`Listening at http://localhost:${port}`);
|
||||
Logger.log('');
|
||||
});
|
||||
}
|
||||
|
||||
function logLogo() {
|
||||
Logger.log(' ________ __ ____ ___');
|
||||
Logger.log(' / ____/ /_ ____ _____/ /_/ __/___ / (_)___');
|
||||
Logger.log(' / / __/ __ \\/ __ \\/ ___/ __/ /_/ __ \\/ / / __ \\');
|
||||
Logger.log('/ /_/ / / / / /_/ (__ ) /_/ __/ /_/ / / / /_/ /');
|
||||
Logger.log(
|
||||
`\\____/_/ /_/\\____/____/\\__/_/ \\____/_/_/\\____/ ${environment.version}`
|
||||
);
|
||||
Logger.log('');
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"1INCH": "1inch",
|
||||
"ALGO": "Algorand",
|
||||
"AVAX": "Avalanche",
|
||||
"MATIC": "Polygon",
|
||||
"SHIB": "Shiba Inu"
|
||||
|
@ -18,7 +18,6 @@ import {
|
||||
|
||||
import { ConfigurationService } from './configuration.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 { ExchangeRateDataService } from './exchange-rate-data.service';
|
||||
import { IDataGatheringItem } from './interfaces/interfaces';
|
||||
@ -26,13 +25,14 @@ import { PrismaService } from './prisma.service';
|
||||
|
||||
@Injectable()
|
||||
export class DataGatheringService {
|
||||
private dataGatheringProgress: number;
|
||||
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService,
|
||||
@Inject('DataEnhancers')
|
||||
private readonly dataEnhancers: DataEnhancerInterface[],
|
||||
private readonly dataProviderService: DataProviderService,
|
||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||
private readonly ghostfolioScraperApi: GhostfolioScraperApiService,
|
||||
private readonly prismaService: PrismaService,
|
||||
private readonly symbolProfileService: SymbolProfileService
|
||||
) {}
|
||||
@ -204,8 +204,11 @@ export class DataGatheringService {
|
||||
|
||||
public async gatherSymbols(aSymbolsWithStartDate: IDataGatheringItem[]) {
|
||||
let hasError = false;
|
||||
let symbolCounter = 0;
|
||||
|
||||
for (const { dataSource, date, symbol } of aSymbolsWithStartDate) {
|
||||
this.dataGatheringProgress = symbolCounter / aSymbolsWithStartDate.length;
|
||||
|
||||
try {
|
||||
const historicalData = await this.dataProviderService.getHistoricalRaw(
|
||||
[{ dataSource, symbol }],
|
||||
@ -263,6 +266,16 @@ export class DataGatheringService {
|
||||
hasError = true;
|
||||
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();
|
||||
@ -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() {
|
||||
return await this.prismaService.property.findUnique({
|
||||
where: { key: 'LOCKED_DATA_GATHERING' }
|
||||
|
@ -197,16 +197,20 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
// filter out undefined symbols
|
||||
return quote.symbol;
|
||||
})
|
||||
.filter(({ quoteType }) => {
|
||||
.filter(({ quoteType, symbol }) => {
|
||||
return (
|
||||
quoteType === 'CRYPTOCURRENCY' ||
|
||||
(quoteType === 'CRYPTOCURRENCY' &&
|
||||
this.cryptocurrencyService.isCrypto(
|
||||
symbol.replace(new RegExp('-USD$'), 'USD').replace('1', '')
|
||||
)) ||
|
||||
quoteType === 'EQUITY' ||
|
||||
quoteType === 'ETF'
|
||||
);
|
||||
})
|
||||
.filter(({ quoteType, symbol }) => {
|
||||
if (quoteType === 'CRYPTOCURRENCY') {
|
||||
// Only allow cryptocurrencies in USD
|
||||
// Only allow cryptocurrencies in USD to avoid having redundancy in the database.
|
||||
// Trades need to be converted manually before to USD (or a UI converter needs to be developed)
|
||||
return symbol.includes('USD');
|
||||
}
|
||||
|
||||
@ -254,14 +258,15 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
if (isCurrency(aSymbol.substring(0, aSymbol.length - 3))) {
|
||||
return `${aSymbol}=X`;
|
||||
} else if (
|
||||
this.cryptocurrencyService.isCrypto(aSymbol) ||
|
||||
this.cryptocurrencyService.isCrypto(aSymbol.replace('1', ''))
|
||||
this.cryptocurrencyService.isCrypto(
|
||||
aSymbol.replace(new RegExp('-USD$'), 'USD').replace('1', '')
|
||||
)
|
||||
) {
|
||||
// Add a dash before the last three characters
|
||||
// BTCUSD -> BTC-USD
|
||||
// DOGEUSD -> DOGE-USD
|
||||
// SOL1USD -> SOL1-USD
|
||||
return aSymbol.replace('USD', '-USD');
|
||||
return aSymbol.replace(new RegExp('-?USD$'), '-USD');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,6 @@
|
||||
"emitDecoratorMetadata": true,
|
||||
"target": "es2015"
|
||||
},
|
||||
"exclude": ["**/*.spec.ts"],
|
||||
"exclude": ["**/*.spec.ts", "**/*.test.ts"],
|
||||
"include": ["**/*.ts"]
|
||||
}
|
||||
|
@ -5,5 +5,5 @@
|
||||
"module": "commonjs",
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"include": ["**/*.spec.ts", "**/*.d.ts"]
|
||||
"include": ["**/*.spec.ts", "**/*.test.ts", "**/*.d.ts"]
|
||||
}
|
||||
|
@ -14,5 +14,8 @@ module.exports = {
|
||||
'jest-preset-angular/build/serializers/ng-snapshot',
|
||||
'jest-preset-angular/build/serializers/html-comment'
|
||||
],
|
||||
transform: { '^.+\\.(ts|js|html)$': 'jest-preset-angular' }
|
||||
transform: {
|
||||
'^.+.(ts|mjs|js|html)$': 'jest-preset-angular'
|
||||
},
|
||||
transformIgnorePatterns: ['node_modules/(?!.*.mjs$)']
|
||||
};
|
||||
|
@ -15,6 +15,24 @@
|
||||
>(Default)</span
|
||||
>
|
||||
</td>
|
||||
<td *matFooterCellDef class="px-1" mat-footer-cell>Total</td>
|
||||
</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>
|
||||
<td *matFooterCellDef class="d-none d-lg-table-cell px-1" mat-footer-cell>
|
||||
{{ baseCurrency }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="platform">
|
||||
@ -37,6 +55,11 @@
|
||||
<span>{{ element.Platform?.name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
*matFooterCellDef
|
||||
class="d-none d-lg-table-cell px-1"
|
||||
mat-footer-cell
|
||||
></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="transactions">
|
||||
@ -45,7 +68,12 @@
|
||||
<span class="d-none d-sm-block" i18n>Transactions</span>
|
||||
</th>
|
||||
<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 *matFooterCellDef class="px-1 text-right" mat-footer-cell>
|
||||
{{ transactionCount }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@ -56,9 +84,39 @@
|
||||
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
||||
<gf-value
|
||||
class="d-inline-block justify-content-end"
|
||||
[currency]="element.currency"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="element.balance"
|
||||
[value]="element.convertedBalance"
|
||||
></gf-value>
|
||||
</td>
|
||||
<td *matFooterCellDef class="px-1 text-right" mat-footer-cell>
|
||||
<gf-value
|
||||
class="d-inline-block justify-content-end"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="totalBalance"
|
||||
></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>
|
||||
</td>
|
||||
<td *matFooterCellDef class="px-1 text-right" mat-footer-cell>
|
||||
<gf-value
|
||||
class="d-inline-block justify-content-end"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="totalValue"
|
||||
></gf-value>
|
||||
</td>
|
||||
</ng-container>
|
||||
@ -88,10 +146,16 @@
|
||||
</button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
|
||||
</ng-container>
|
||||
|
||||
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
||||
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
|
||||
<tr
|
||||
*matFooterRowDef="displayedColumns"
|
||||
mat-footer-row
|
||||
[ngClass]="{ 'd-none': isLoading }"
|
||||
></tr>
|
||||
</table>
|
||||
|
||||
<ngx-skeleton-loader
|
||||
|
@ -10,6 +10,16 @@
|
||||
}
|
||||
|
||||
.mat-table {
|
||||
td {
|
||||
&.mat-footer-cell {
|
||||
border-top: 1px solid
|
||||
rgba(
|
||||
var(--palette-foreground-divider),
|
||||
var(--palette-foreground-divider-alpha)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
th {
|
||||
::ng-deep {
|
||||
.mat-sort-header-container {
|
||||
@ -21,7 +31,14 @@
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
.mat-form-field {
|
||||
color: rgba(var(--light-primary-text));
|
||||
.mat-table {
|
||||
td {
|
||||
&.mat-footer-cell {
|
||||
border-top-color: rgba(
|
||||
var(--palette-foreground-divider-dark),
|
||||
var(--palette-foreground-divider-dark-alpha)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,9 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
|
||||
@Input() deviceType: string;
|
||||
@Input() locale: string;
|
||||
@Input() showActions: boolean;
|
||||
@Input() totalBalance: number;
|
||||
@Input() totalValue: number;
|
||||
@Input() transactionCount: number;
|
||||
|
||||
@Output() accountDeleted = new EventEmitter<string>();
|
||||
@Output() accountToUpdate = new EventEmitter<AccountModel>();
|
||||
@ -41,7 +44,14 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
|
||||
public ngOnInit() {}
|
||||
|
||||
public ngOnChanges() {
|
||||
this.displayedColumns = ['account', 'platform', 'transactions', 'balance'];
|
||||
this.displayedColumns = [
|
||||
'account',
|
||||
'currency',
|
||||
'platform',
|
||||
'transactions',
|
||||
'balance',
|
||||
'value'
|
||||
];
|
||||
|
||||
if (this.showActions) {
|
||||
this.displayedColumns.push('actions');
|
||||
|
@ -1,4 +1,8 @@
|
||||
<span class="flex-grow-1 text-truncate">{{ title }}</span>
|
||||
<span
|
||||
class="flex-grow-1 text-truncate"
|
||||
[ngClass]="{ 'text-center': position === 'center' }"
|
||||
>{{ title }}</span
|
||||
>
|
||||
<button
|
||||
*ngIf="deviceType !== 'mobile'"
|
||||
class="no-min-width px-0"
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
})
|
||||
export class DialogHeaderComponent implements OnInit {
|
||||
@Input() deviceType: string;
|
||||
@Input() position: 'center' | 'left' = 'left';
|
||||
@Input() title: string;
|
||||
|
||||
@Output() closeButtonClicked = new EventEmitter<void>();
|
||||
|
@ -270,6 +270,7 @@
|
||||
Sign In
|
||||
</button>
|
||||
<a
|
||||
*ngIf="currentRoute !== 'register'"
|
||||
class="d-none d-sm-block"
|
||||
color="primary"
|
||||
i18n
|
||||
|
@ -2,6 +2,5 @@ import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.i
|
||||
|
||||
export interface PositionDetailDialogParams {
|
||||
deviceType: string;
|
||||
fearAndGreedIndex: number;
|
||||
historicalDataItems: LineChartItem[];
|
||||
}
|
||||
|
@ -22,13 +22,11 @@ import { PositionDetailDialogParams } from './interfaces/interfaces';
|
||||
})
|
||||
export class PerformanceChartDialog {
|
||||
public benchmarkDataItems: LineChartItem[];
|
||||
public benchmarkLabel = 'S&P 500';
|
||||
public benchmarkSymbol = 'VOO';
|
||||
public currency: string;
|
||||
public firstBuyDate: string;
|
||||
public marketPrice: number;
|
||||
public historicalDataItems: LineChartItem[];
|
||||
public title: string;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
@ -83,8 +81,6 @@ export class PerformanceChartDialog {
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.title = `Performance vs. ${this.benchmarkLabel}`;
|
||||
}
|
||||
|
||||
public onClose(): void {
|
||||
|
@ -1,7 +1,7 @@
|
||||
<gf-dialog-header
|
||||
mat-dialog-title
|
||||
title="Performance"
|
||||
[deviceType]="data.deviceType"
|
||||
[title]="title"
|
||||
(closeButtonClicked)="onClose()"
|
||||
></gf-dialog-header>
|
||||
|
||||
@ -11,7 +11,6 @@
|
||||
class="mb-4"
|
||||
symbol="Performance"
|
||||
[benchmarkDataItems]="benchmarkDataItems"
|
||||
[benchmarkLabel]="benchmarkLabel"
|
||||
[historicalDataItems]="historicalDataItems"
|
||||
[showGradient]="true"
|
||||
[showLegend]="true"
|
||||
@ -19,13 +18,6 @@
|
||||
[showYAxis]="false"
|
||||
></gf-line-chart>
|
||||
</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>
|
||||
|
||||
<gf-dialog-footer
|
||||
|
@ -8,7 +8,6 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
|
||||
import { GfDialogFooterModule } from '../dialog-footer/dialog-footer.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';
|
||||
|
||||
@NgModule({
|
||||
@ -18,7 +17,6 @@ import { PerformanceChartDialog } from './performance-chart-dialog.component';
|
||||
CommonModule,
|
||||
GfDialogFooterModule,
|
||||
GfDialogHeaderModule,
|
||||
GfFearAndGreedIndexModule,
|
||||
GfLineChartModule,
|
||||
GfValueModule,
|
||||
MatButtonModule,
|
||||
|
@ -1,5 +1,11 @@
|
||||
<div class="container p-0">
|
||||
<div class="row no-gutters">
|
||||
<div
|
||||
class="no-gutters row"
|
||||
[ngClass]="{
|
||||
'text-danger': isAllTimeLow,
|
||||
'text-success': isAllTimeHigh
|
||||
}"
|
||||
>
|
||||
<div class="flex-grow-1"></div>
|
||||
<div *ngIf="isLoading" class="align-items-center d-flex">
|
||||
<ngx-skeleton-loader
|
||||
@ -12,8 +18,8 @@
|
||||
></ngx-skeleton-loader>
|
||||
</div>
|
||||
<div
|
||||
[hidden]="isLoading"
|
||||
class="display-4 font-weight-bold m-0 text-center value-container"
|
||||
[hidden]="isLoading"
|
||||
>
|
||||
<span #value id="value"></span>
|
||||
</div>
|
||||
|
@ -19,6 +19,8 @@ import { isNumber } from 'lodash';
|
||||
})
|
||||
export class PortfolioPerformanceComponent implements OnChanges, OnInit {
|
||||
@Input() baseCurrency: string;
|
||||
@Input() isAllTimeHigh: boolean;
|
||||
@Input() isAllTimeLow: boolean;
|
||||
@Input() isLoading: boolean;
|
||||
@Input() locale: string;
|
||||
@Input() performance: PortfolioPerformance;
|
||||
|
@ -77,7 +77,7 @@
|
||||
<div class="row px-3 py-1">
|
||||
<div class="d-flex flex-grow-1" i18n>
|
||||
Fees for {{ summary?.ordersCount }} {summary?.ordersCount, plural, =1
|
||||
{order} other {orders}}
|
||||
{transaction} other {transactions}}
|
||||
</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<span *ngIf="summary?.fees || summary?.fees === 0" class="mr-1">-</span>
|
||||
@ -132,7 +132,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row px-3 py-1">
|
||||
<div class="d-flex flex-grow-1" i18n>Cash</div>
|
||||
<div class="d-flex flex-grow-1" i18n>Cash (Buying Power)</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
|
@ -9,6 +9,7 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||
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 { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
@ -23,6 +24,7 @@ import { PositionDetailDialogParams } from './interfaces/interfaces';
|
||||
styleUrls: ['./position-detail-dialog.component.scss']
|
||||
})
|
||||
export class PositionDetailDialog implements OnDestroy {
|
||||
public assetSubClass: AssetSubClass;
|
||||
public averagePrice: number;
|
||||
public benchmarkDataItems: LineChartItem[];
|
||||
public currency: string;
|
||||
@ -38,8 +40,10 @@ export class PositionDetailDialog implements OnDestroy {
|
||||
public netPerformance: number;
|
||||
public netPerformancePercent: number;
|
||||
public quantity: number;
|
||||
public quantityPrecision = 2;
|
||||
public symbol: string;
|
||||
public transactionCount: number;
|
||||
public value: number;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
@ -54,6 +58,7 @@ export class PositionDetailDialog implements OnDestroy {
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(
|
||||
({
|
||||
assetSubClass,
|
||||
averagePrice,
|
||||
currency,
|
||||
firstBuyDate,
|
||||
@ -69,8 +74,10 @@ export class PositionDetailDialog implements OnDestroy {
|
||||
netPerformancePercent,
|
||||
quantity,
|
||||
symbol,
|
||||
transactionCount
|
||||
transactionCount,
|
||||
value
|
||||
}) => {
|
||||
this.assetSubClass = assetSubClass;
|
||||
this.averagePrice = averagePrice;
|
||||
this.benchmarkDataItems = [];
|
||||
this.currency = currency;
|
||||
@ -100,6 +107,7 @@ export class PositionDetailDialog implements OnDestroy {
|
||||
this.quantity = quantity;
|
||||
this.symbol = symbol;
|
||||
this.transactionCount = transactionCount;
|
||||
this.value = value;
|
||||
|
||||
if (isToday(parseISO(this.firstBuyDate))) {
|
||||
// Add average price
|
||||
@ -146,6 +154,18 @@ export class PositionDetailDialog implements OnDestroy {
|
||||
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();
|
||||
}
|
||||
);
|
||||
|
@ -1,5 +1,6 @@
|
||||
<gf-dialog-header
|
||||
mat-dialog-title
|
||||
position="center"
|
||||
[deviceType]="data.deviceType"
|
||||
[title]="name ?? symbol"
|
||||
(closeButtonClicked)="onClose()"
|
||||
@ -7,6 +8,17 @@
|
||||
|
||||
<div class="flex-grow-1" mat-dialog-content>
|
||||
<div class="container p-0">
|
||||
<div class="row">
|
||||
<div class="col-12 d-flex justify-content-center mb-3">
|
||||
<gf-value
|
||||
size="large"
|
||||
[currency]="data.baseCurrency"
|
||||
[locale]="data.locale"
|
||||
[value]="value"
|
||||
></gf-value>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<gf-line-chart
|
||||
class="mb-4"
|
||||
benchmarkLabel="Buy Price"
|
||||
@ -82,7 +94,7 @@
|
||||
label="Quantity"
|
||||
size="medium"
|
||||
[locale]="data.locale"
|
||||
[precision]="2"
|
||||
[precision]="quantityPrecision"
|
||||
[value]="quantity"
|
||||
></gf-value>
|
||||
</div>
|
||||
|
@ -15,6 +15,27 @@
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="d-none d-lg-table-cell justify-content-end px-1"
|
||||
i18n
|
||||
mat-header-cell
|
||||
mat-sort-header
|
||||
>
|
||||
Value
|
||||
</th>
|
||||
<td class="d-none d-lg-table-cell px-1" mat-cell *matCellDef="let element">
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-value
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="isLoading ? undefined : element.value"
|
||||
></gf-value>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="performance">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
@ -103,7 +124,9 @@
|
||||
></ngx-skeleton-loader>
|
||||
|
||||
<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
|
||||
|
@ -70,6 +70,7 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
|
||||
public ngOnChanges() {
|
||||
this.displayedColumns = [
|
||||
'symbol',
|
||||
'value',
|
||||
'performance',
|
||||
'allocationInvestment',
|
||||
'allocationCurrent'
|
||||
|
@ -24,7 +24,9 @@
|
||||
></gf-position>
|
||||
</ng-container>
|
||||
<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>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
@ -2,7 +2,9 @@
|
||||
<div class="row no-gutters">
|
||||
<div class="col">
|
||||
<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>
|
||||
|
||||
<gf-rule *ngIf="rules === undefined" [isLoading]="true"></gf-rule>
|
||||
|
@ -254,10 +254,12 @@
|
||||
*matRowDef="let row; columns: displayedColumns"
|
||||
mat-row
|
||||
(click)="
|
||||
onOpenPositionDialog({
|
||||
symbol: row.symbol
|
||||
})
|
||||
!row.isDraft &&
|
||||
onOpenPositionDialog({
|
||||
symbol: row.symbol
|
||||
})
|
||||
"
|
||||
[ngClass]="{ 'is-draft': row.isDraft }"
|
||||
></tr>
|
||||
</table>
|
||||
|
||||
@ -270,3 +272,9 @@
|
||||
width: '100%'
|
||||
}"
|
||||
></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>
|
||||
|
@ -24,7 +24,9 @@
|
||||
}
|
||||
|
||||
.mat-row {
|
||||
cursor: pointer;
|
||||
&:not(.is-draft) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.type-badge {
|
||||
background-color: rgba(var(--palette-foreground-text), 0.05);
|
||||
|
@ -10,6 +10,7 @@ import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
|
||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
|
||||
@ -22,6 +23,7 @@ import { TransactionsTableComponent } from './transactions-table.component';
|
||||
exports: [TransactionsTableComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfNoTransactionsInfoModule,
|
||||
GfPositionDetailDialogModule,
|
||||
GfSymbolIconModule,
|
||||
GfSymbolModule,
|
||||
|
@ -28,6 +28,9 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
||||
public hasPermissionToCreateAccount: boolean;
|
||||
public hasPermissionToDeleteAccount: boolean;
|
||||
public routeQueryParams: Subscription;
|
||||
public totalBalance = 0;
|
||||
public totalValue = 0;
|
||||
public transactionCount = 0;
|
||||
public user: User;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
@ -103,8 +106,11 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
||||
this.dataService
|
||||
.fetchAccounts()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((response) => {
|
||||
this.accounts = response;
|
||||
.subscribe(({ accounts, totalBalance, totalValue, transactionCount }) => {
|
||||
this.accounts = accounts;
|
||||
this.totalBalance = totalBalance;
|
||||
this.totalValue = totalValue;
|
||||
this.transactionCount = transactionCount;
|
||||
|
||||
if (this.accounts?.length <= 0) {
|
||||
this.router.navigate([], { queryParams: { createDialog: true } });
|
||||
|
@ -8,6 +8,9 @@
|
||||
[deviceType]="deviceType"
|
||||
[locale]="user?.settings?.locale"
|
||||
[showActions]="!hasImpersonationId && hasPermissionToDeleteAccount && !user.settings.isRestrictedView"
|
||||
[totalBalance]="totalBalance"
|
||||
[totalValue]="totalValue"
|
||||
[transactionCount]="transactionCount"
|
||||
(accountDeleted)="onDeleteAccount($event)"
|
||||
(accountToUpdate)="onUpdateAccount($event)"
|
||||
></gf-accounts-table>
|
||||
|
@ -22,6 +22,7 @@ import { takeUntil } from 'rxjs/operators';
|
||||
})
|
||||
export class AdminPageComponent implements OnDestroy, OnInit {
|
||||
public dataGatheringInProgress: boolean;
|
||||
public dataGatheringProgress: number;
|
||||
public defaultDateFormat = DEFAULT_DATE_FORMAT;
|
||||
public exchangeRates: { label1: string; label2: string; value: number }[];
|
||||
public lastDataGathering: string;
|
||||
@ -134,12 +135,14 @@ export class AdminPageComponent implements OnDestroy, OnInit {
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(
|
||||
({
|
||||
dataGatheringProgress,
|
||||
exchangeRates,
|
||||
lastDataGathering,
|
||||
transactionCount,
|
||||
userCount,
|
||||
users
|
||||
}) => {
|
||||
this.dataGatheringProgress = dataGatheringProgress;
|
||||
this.exchangeRates = exchangeRates;
|
||||
this.users = users;
|
||||
|
||||
|
@ -35,14 +35,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<ng-container *ngIf="lastDataGathering"
|
||||
>{{ lastDataGathering }}</ng-container
|
||||
>
|
||||
<ng-container *ngIf="dataGatheringInProgress" i18n
|
||||
>In Progress</ng-container
|
||||
>In Progress ({{ dataGatheringProgress | percent : '1.2-2'
|
||||
}})</ng-container
|
||||
>
|
||||
</div>
|
||||
<div class="mt-2 overflow-hidden">
|
||||
|
@ -7,10 +7,7 @@ import {
|
||||
OnInit,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
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 { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
@ -61,8 +58,9 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
||||
public hasImpersonationId: boolean;
|
||||
public hasPermissionToAccessFearAndGreedIndex: boolean;
|
||||
public hasPermissionToCreateOrder: boolean;
|
||||
public hasPositions: boolean;
|
||||
public historicalDataItems: LineChartItem[];
|
||||
public isAllTimeHigh: boolean;
|
||||
public isAllTimeLow: boolean;
|
||||
public isLoadingPerformance = true;
|
||||
public isLoadingSummary = true;
|
||||
public performance: PortfolioPerformance;
|
||||
@ -80,21 +78,10 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
private deviceService: DeviceDetectorService,
|
||||
private dialog: MatDialog,
|
||||
private impersonationStorageService: ImpersonationStorageService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private settingsStorageService: SettingsStorageService,
|
||||
private userService: UserService
|
||||
) {
|
||||
this.routeQueryParams = this.route.queryParams
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((params) => {
|
||||
if (params['performanceChartDialog']) {
|
||||
this.openDialog();
|
||||
}
|
||||
});
|
||||
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((state) => {
|
||||
@ -173,25 +160,6 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
||||
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() {
|
||||
if (this.currentTabIndex === 0) {
|
||||
this.isLoadingPerformance = true;
|
||||
@ -200,12 +168,14 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
||||
.fetchChart({ range: this.dateRange })
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((chartData) => {
|
||||
this.historicalDataItems = chartData.map((chartDataItem) => {
|
||||
this.historicalDataItems = chartData.chart.map((chartDataItem) => {
|
||||
return {
|
||||
date: chartDataItem.date,
|
||||
value: chartDataItem.value
|
||||
};
|
||||
});
|
||||
this.isAllTimeHigh = chartData.isAllTimeHigh;
|
||||
this.isAllTimeLow = chartData.isAllTimeLow;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
@ -225,7 +195,6 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((response) => {
|
||||
this.positions = response.positions;
|
||||
this.hasPositions = this.positions?.length > 0;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
@ -23,12 +23,7 @@
|
||||
"
|
||||
>
|
||||
<div class="row w-100">
|
||||
<a
|
||||
*ngIf="historicalDataItems?.length !== 0"
|
||||
class="chart-container col"
|
||||
[routerLink]="[]"
|
||||
[queryParams]="{performanceChartDialog: true}"
|
||||
>
|
||||
<div class="chart-container col">
|
||||
<gf-line-chart
|
||||
class="mr-3"
|
||||
symbol="Performance"
|
||||
@ -38,19 +33,19 @@
|
||||
[showXAxis]="false"
|
||||
[showYAxis]="false"
|
||||
></gf-line-chart>
|
||||
</a>
|
||||
<div
|
||||
*ngIf="historicalDataItems?.length === 0"
|
||||
class="
|
||||
align-items-center
|
||||
chart-container
|
||||
d-flex
|
||||
justify-content-center
|
||||
w-100
|
||||
"
|
||||
>
|
||||
<div class="d-flex justify-content-center">
|
||||
<gf-no-transactions-info-indicator></gf-no-transactions-info-indicator>
|
||||
<div
|
||||
*ngIf="historicalDataItems?.length === 0"
|
||||
class="
|
||||
align-items-center
|
||||
chart-container
|
||||
d-flex
|
||||
justify-content-center
|
||||
w-100
|
||||
"
|
||||
>
|
||||
<div class="d-flex justify-content-center">
|
||||
<gf-no-transactions-info-indicator></gf-no-transactions-info-indicator>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -59,6 +54,8 @@
|
||||
<gf-portfolio-performance
|
||||
class="pb-4"
|
||||
[baseCurrency]="user?.settings?.baseCurrency"
|
||||
[isAllTimeHigh]="isAllTimeHigh"
|
||||
[isAllTimeLow]="isAllTimeLow"
|
||||
[isLoading]="isLoadingPerformance"
|
||||
[locale]="user?.settings?.locale"
|
||||
[performance]="performance"
|
||||
@ -92,33 +89,25 @@
|
||||
(change)="onChangeDateRange($event.value)"
|
||||
></gf-toggle>
|
||||
</div>
|
||||
<ng-container *ngIf="hasPositions === true">
|
||||
<mat-card class="p-0">
|
||||
<mat-card-content>
|
||||
<gf-positions
|
||||
[baseCurrency]="user?.settings?.baseCurrency"
|
||||
[deviceType]="deviceType"
|
||||
[locale]="user?.settings?.locale"
|
||||
[positions]="positions"
|
||||
[range]="dateRange"
|
||||
></gf-positions>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<div *ngIf="hasPermissionToCreateOrder" class="text-center">
|
||||
<a
|
||||
class="mt-3"
|
||||
i18n
|
||||
mat-button
|
||||
[routerLink]="['/portfolio', 'transactions']"
|
||||
>Manage Transactions...</a
|
||||
>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div
|
||||
*ngIf="hasPositions === false"
|
||||
class="d-flex justify-content-center"
|
||||
>
|
||||
<gf-no-transactions-info-indicator></gf-no-transactions-info-indicator>
|
||||
<mat-card class="p-0">
|
||||
<mat-card-content>
|
||||
<gf-positions
|
||||
[baseCurrency]="user?.settings?.baseCurrency"
|
||||
[deviceType]="deviceType"
|
||||
[locale]="user?.settings?.locale"
|
||||
[positions]="positions"
|
||||
[range]="dateRange"
|
||||
></gf-positions>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<div *ngIf="hasPermissionToCreateOrder" class="text-center">
|
||||
<a
|
||||
class="mt-3"
|
||||
i18n
|
||||
mat-button
|
||||
[routerLink]="['/portfolio', 'transactions']"
|
||||
>Manage Transactions...</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -148,4 +137,33 @@
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
|
@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
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 { GfPortfolioPerformanceModule } from '@ghostfolio/client/components/portfolio-performance/portfolio-performance.module';
|
||||
import { GfPortfolioSummaryModule } from '@ghostfolio/client/components/portfolio-summary/portfolio-summary.module';
|
||||
@ -20,6 +21,7 @@ import { HomePageComponent } from './home-page.component';
|
||||
exports: [],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfFearAndGreedIndexModule,
|
||||
GfLineChartModule,
|
||||
GfNoTransactionsInfoModule,
|
||||
GfPerformanceChartDialogModule,
|
||||
|
@ -27,7 +27,6 @@
|
||||
&.overview {
|
||||
.chart-container {
|
||||
aspect-ratio: 16 / 9;
|
||||
cursor: pointer;
|
||||
max-height: 50vh;
|
||||
|
||||
// Fallback for aspect-ratio (using padding hack)
|
||||
|
@ -14,6 +14,28 @@ import { Subject } from 'rxjs';
|
||||
export class LandingPageComponent implements OnDestroy, OnInit {
|
||||
public currentYear = format(new Date(), 'yyyy');
|
||||
public demoAuthToken: string;
|
||||
public testimonials = [
|
||||
{
|
||||
author: 'Philipp',
|
||||
country: 'Germany 🇩🇪',
|
||||
quote: `Super slim app with a great user interface. On top of that, it's open source.`
|
||||
},
|
||||
{
|
||||
author: 'Onur',
|
||||
country: 'Switzerland 🇨🇭',
|
||||
quote: `Ghostfolio looks like the perfect portfolio tracker I've been searching for all these years.`
|
||||
},
|
||||
{
|
||||
author: 'Ivo',
|
||||
country: 'Netherlands 🇳🇱',
|
||||
quote: `A fantastic open source app to track my investments across platforms. Love the simplicity of its design and the depth of the insights.`
|
||||
},
|
||||
{
|
||||
author: 'Damjan',
|
||||
country: 'Slovenia 🇸🇮',
|
||||
quote: `Ghostfolio helps me track all my investments in one place, it has a built-in portfolio analyzer and a very neat, seamless user interface.`
|
||||
}
|
||||
];
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
|
@ -107,6 +107,31 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row my-5">
|
||||
<div class="col-12">
|
||||
<h2 class="h4 mb-1 text-center">
|
||||
What our <strong>users</strong> are saying
|
||||
</h2>
|
||||
</div>
|
||||
<div *ngFor="let testimonial of testimonials" class="col-md-6">
|
||||
<div class="d-flex flex-row py-3">
|
||||
<div class="d-flex justify-content-center">
|
||||
<gf-logo
|
||||
class="mr-3 mt-2 pt-1"
|
||||
size="medium"
|
||||
[hideName]="true"
|
||||
></gf-logo>
|
||||
</div>
|
||||
<div>
|
||||
<div>{{ testimonial.quote }}</div>
|
||||
<div class="mt-2 text-muted">
|
||||
— {{ testimonial.author }}, {{ testimonial.country }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row my-5">
|
||||
<div class="col-md-6 offset-md-3">
|
||||
<h2 class="h4 mb-1 text-center">
|
||||
|
@ -84,6 +84,10 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
|
||||
})
|
||||
);
|
||||
|
||||
if (this.data.transaction.id) {
|
||||
this.searchSymbolCtrl.disable();
|
||||
}
|
||||
|
||||
if (this.data.transaction.symbol) {
|
||||
this.dataService
|
||||
.fetchSymbolItem({
|
||||
|
@ -10,9 +10,7 @@
|
||||
required
|
||||
[(value)]="data.transaction.accountId"
|
||||
>
|
||||
<mat-option
|
||||
*ngFor="let account of data.user?.accounts"
|
||||
[value]="account.id"
|
||||
<mat-option *ngFor="let account of data.accounts" [value]="account.id"
|
||||
>{{ account.name }}</mat-option
|
||||
>
|
||||
</mat-select>
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { User } from '@ghostfolio/common/interfaces';
|
||||
import { Order } from '@prisma/client';
|
||||
import { Account, Order } from '@prisma/client';
|
||||
|
||||
export interface CreateOrUpdateTransactionDialogParams {
|
||||
accountId: string;
|
||||
accounts: Account[];
|
||||
transaction: Order;
|
||||
user: User;
|
||||
}
|
||||
|
@ -261,6 +261,9 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
||||
}: OrderModel): void {
|
||||
const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, {
|
||||
data: {
|
||||
accounts: this.user?.accounts?.filter((account) => {
|
||||
return account.accountType === 'SECURITIES';
|
||||
}),
|
||||
transaction: {
|
||||
accountId,
|
||||
currency,
|
||||
@ -343,6 +346,9 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
||||
private openCreateTransactionDialog(aTransaction?: OrderModel): void {
|
||||
const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, {
|
||||
data: {
|
||||
accounts: this.user?.accounts?.filter((account) => {
|
||||
return account.accountType === 'SECURITIES';
|
||||
}),
|
||||
transaction: {
|
||||
accountId: aTransaction?.accountId ?? this.defaultAccountId,
|
||||
currency: aTransaction?.currency ?? null,
|
||||
|
@ -38,8 +38,9 @@ export class ZenPageComponent implements AfterViewInit, OnDestroy, OnInit {
|
||||
public deviceType: string;
|
||||
public hasImpersonationId: boolean;
|
||||
public hasPermissionToCreateOrder: boolean;
|
||||
public hasPositions: boolean;
|
||||
public historicalDataItems: LineChartItem[];
|
||||
public isAllTimeHigh: boolean;
|
||||
public isAllTimeLow: boolean;
|
||||
public isLoadingPerformance = true;
|
||||
public performance: PortfolioPerformance;
|
||||
public positions: Position[];
|
||||
@ -115,12 +116,14 @@ export class ZenPageComponent implements AfterViewInit, OnDestroy, OnInit {
|
||||
.fetchChart({ range: this.dateRange })
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((chartData) => {
|
||||
this.historicalDataItems = chartData.map((chartDataItem) => {
|
||||
this.historicalDataItems = chartData.chart.map((chartDataItem) => {
|
||||
return {
|
||||
date: chartDataItem.date,
|
||||
value: chartDataItem.value
|
||||
};
|
||||
});
|
||||
this.isAllTimeHigh = chartData.isAllTimeHigh;
|
||||
this.isAllTimeLow = chartData.isAllTimeLow;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
@ -140,7 +143,6 @@ export class ZenPageComponent implements AfterViewInit, OnDestroy, OnInit {
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((response) => {
|
||||
this.positions = response.positions;
|
||||
this.hasPositions = this.positions?.length > 0;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
@ -47,6 +47,8 @@
|
||||
<gf-portfolio-performance
|
||||
class="pb-4"
|
||||
[baseCurrency]="user?.settings?.baseCurrency"
|
||||
[isAllTimeHigh]="isAllTimeHigh"
|
||||
[isAllTimeLow]="isAllTimeLow"
|
||||
[isLoading]="isLoadingPerformance"
|
||||
[locale]="user?.settings?.locale"
|
||||
[performance]="performance"
|
||||
@ -65,35 +67,25 @@
|
||||
<h3 class="d-flex justify-content-center mb-3" i18n>Holdings</h3>
|
||||
<div class="row">
|
||||
<div class="align-items-center col">
|
||||
<ng-container *ngIf="hasPositions === true">
|
||||
<mat-card class="p-0">
|
||||
<mat-card-content>
|
||||
<gf-positions
|
||||
[baseCurrency]="user?.settings?.baseCurrency"
|
||||
[deviceType]="deviceType"
|
||||
[locale]="user?.settings?.locale"
|
||||
[positions]="positions"
|
||||
[range]="dateRange"
|
||||
></gf-positions>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<div *ngIf="hasPermissionToCreateOrder" class="text-center">
|
||||
<a
|
||||
class="mt-3"
|
||||
i18n
|
||||
mat-button
|
||||
[routerLink]="['/portfolio', 'transactions']"
|
||||
>Manage Transactions...</a
|
||||
>
|
||||
</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>
|
||||
<mat-card class="p-0">
|
||||
<mat-card-content>
|
||||
<gf-positions
|
||||
[baseCurrency]="user?.settings?.baseCurrency"
|
||||
[deviceType]="deviceType"
|
||||
[locale]="user?.settings?.locale"
|
||||
[positions]="positions"
|
||||
[range]="dateRange"
|
||||
></gf-positions>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<div *ngIf="hasPermissionToCreateOrder" class="text-center">
|
||||
<a
|
||||
class="mt-3"
|
||||
i18n
|
||||
mat-button
|
||||
[routerLink]="['/portfolio', 'transactions']"
|
||||
>Manage Transactions...</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -5,10 +5,7 @@ import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto
|
||||
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
|
||||
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
||||
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
||||
import {
|
||||
HistoricalDataItem,
|
||||
PortfolioPositionDetail
|
||||
} from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position-detail.interface';
|
||||
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';
|
||||
@ -17,9 +14,11 @@ import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setti
|
||||
import { UpdateUserSettingsDto } from '@ghostfolio/api/app/user/update-user-settings.dto';
|
||||
import {
|
||||
Access,
|
||||
Accounts,
|
||||
AdminData,
|
||||
Export,
|
||||
InfoItem,
|
||||
PortfolioChart,
|
||||
PortfolioDetails,
|
||||
PortfolioPerformance,
|
||||
PortfolioPublicDetails,
|
||||
@ -30,11 +29,7 @@ import {
|
||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||
import { permissions } from '@ghostfolio/common/permissions';
|
||||
import { DateRange } from '@ghostfolio/common/types';
|
||||
import {
|
||||
Account as AccountModel,
|
||||
DataSource,
|
||||
Order as OrderModel
|
||||
} from '@prisma/client';
|
||||
import { DataSource, Order as OrderModel } from '@prisma/client';
|
||||
import { parseISO } from 'date-fns';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { Observable } from 'rxjs';
|
||||
@ -62,7 +57,7 @@ export class DataService {
|
||||
}
|
||||
|
||||
public fetchAccounts() {
|
||||
return this.http.get<AccountModel[]>('/api/account');
|
||||
return this.http.get<Accounts>('/api/account');
|
||||
}
|
||||
|
||||
public fetchAdminData() {
|
||||
@ -90,7 +85,7 @@ export class DataService {
|
||||
}
|
||||
|
||||
public fetchChart({ range }: { range: DateRange }) {
|
||||
return this.http.get<HistoricalDataItem[]>('/api/portfolio/chart', {
|
||||
return this.http.get<PortfolioChart>('/api/portfolio/chart', {
|
||||
params: { range }
|
||||
});
|
||||
}
|
||||
|
@ -18,16 +18,6 @@
|
||||
* BROWSER POLYFILLS
|
||||
*/
|
||||
|
||||
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
|
||||
// import 'classlist.js'; // Run `npm install --save classlist.js`.
|
||||
|
||||
/**
|
||||
* Web Animations `@angular/platform-browser/animations`
|
||||
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
|
||||
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
|
||||
*/
|
||||
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
||||
|
||||
/**
|
||||
* By default, zone.js will patch all possible macroTask and DomEvents
|
||||
* user can disable parts of macroTask/DomEvents patch by setting following flags
|
||||
|
@ -3,7 +3,7 @@
|
||||
@import './styles/bootstrap';
|
||||
@import './styles/table';
|
||||
|
||||
@import '~angular-material-css-vars/main';
|
||||
@import '~angular-material-css-vars/src/lib/main';
|
||||
|
||||
@import '~svgmap/dist/svgMap';
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
$mat-css-dark-theme-selector: '.is-dark-theme';
|
||||
|
||||
@import '~angular-material-css-vars/public-util';
|
||||
@import '~angular-material-css-vars/src/lib/public-util';
|
||||
|
||||
$alpha-disabled-text: 0.38;
|
||||
$alpha-hover: 0.04;
|
||||
|
@ -1,8 +1,12 @@
|
||||
@mixin gf-table($darkTheme: false) {
|
||||
background: transparent !important;
|
||||
|
||||
td {
|
||||
border: 0 !important;
|
||||
.mat-footer-row,
|
||||
.mat-row {
|
||||
.mat-cell,
|
||||
.mat-footer-cell {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-row {
|
||||
|
@ -6,5 +6,5 @@
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"files": ["src/test-setup.ts"],
|
||||
"include": ["**/*.spec.ts", "**/*.d.ts"]
|
||||
"include": ["**/*.spec.ts", "**/*.test.ts", "**/*.d.ts"]
|
||||
}
|
||||
|
8
libs/common/src/lib/interfaces/accounts.interface.ts
Normal file
8
libs/common/src/lib/interfaces/accounts.interface.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { AccountWithValue } from '@ghostfolio/common/types';
|
||||
|
||||
export interface Accounts {
|
||||
accounts: AccountWithValue[];
|
||||
totalBalance: number;
|
||||
totalValue: number;
|
||||
transactionCount: number;
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
export interface AdminData {
|
||||
dataGatheringProgress?: number;
|
||||
exchangeRates: { label1: string; label2: string; value: number }[];
|
||||
lastDataGathering: Date | 'IN_PROGRESS';
|
||||
lastDataGathering?: Date | 'IN_PROGRESS';
|
||||
transactionCount: number;
|
||||
userCount: number;
|
||||
users: {
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { Access } from './access.interface';
|
||||
import { Accounts } from './accounts.interface';
|
||||
import { AdminData } from './admin-data.interface';
|
||||
import { Export } from './export.interface';
|
||||
import { InfoItem } from './info-item.interface';
|
||||
import { PortfolioChart } from './portfolio-chart.interface';
|
||||
import { PortfolioDetails } from './portfolio-details.interface';
|
||||
import { PortfolioItem } from './portfolio-item.interface';
|
||||
import { PortfolioOverview } from './portfolio-overview.interface';
|
||||
@ -19,9 +21,11 @@ import { User } from './user.interface';
|
||||
|
||||
export {
|
||||
Access,
|
||||
Accounts,
|
||||
AdminData,
|
||||
Export,
|
||||
InfoItem,
|
||||
PortfolioChart,
|
||||
PortfolioDetails,
|
||||
PortfolioItem,
|
||||
PortfolioOverview,
|
||||
|
@ -0,0 +1,7 @@
|
||||
import { HistoricalDataItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position-detail.interface';
|
||||
|
||||
export interface PortfolioChart {
|
||||
isAllTimeHigh: boolean;
|
||||
isAllTimeLow: boolean;
|
||||
chart: HistoricalDataItem[];
|
||||
}
|
@ -5,4 +5,6 @@ export interface PortfolioPerformance {
|
||||
currentNetPerformance: number;
|
||||
currentNetPerformancePercent: number;
|
||||
currentValue: number;
|
||||
isAllTimeHigh: boolean;
|
||||
isAllTimeLow: boolean;
|
||||
}
|
||||
|
7
libs/common/src/lib/types/account-with-value.type.ts
Normal file
7
libs/common/src/lib/types/account-with-value.type.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { Account as AccountModel } from '@prisma/client';
|
||||
|
||||
export type AccountWithValue = AccountModel & {
|
||||
convertedBalance: number;
|
||||
transactionCount: number;
|
||||
value: number;
|
||||
};
|
@ -1,4 +1,5 @@
|
||||
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 { Granularity } from './granularity.type';
|
||||
import type { OrderWithAccount } from './order-with-account.type';
|
||||
@ -6,6 +7,7 @@ import type { RequestWithUser } from './request-with-user.type';
|
||||
|
||||
export type {
|
||||
AccessWithGranteeUser,
|
||||
AccountWithValue,
|
||||
DateRange,
|
||||
Granularity,
|
||||
OrderWithAccount,
|
||||
|
@ -5,5 +5,5 @@
|
||||
"types": []
|
||||
},
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": ["**/*.spec.ts"]
|
||||
"exclude": ["**/*.spec.ts", "**/*.test.ts"]
|
||||
}
|
||||
|
@ -7,9 +7,13 @@
|
||||
},
|
||||
"include": [
|
||||
"**/*.spec.ts",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.tsx",
|
||||
"**/*.test.tsx",
|
||||
"**/*.spec.js",
|
||||
"**/*.test.js",
|
||||
"**/*.spec.jsx",
|
||||
"**/*.test.jsx",
|
||||
"**/*.d.ts"
|
||||
]
|
||||
}
|
||||
|
@ -3,6 +3,6 @@
|
||||
"compilerOptions": {
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"exclude": ["../**/*.spec.ts"],
|
||||
"exclude": ["../**/*.spec.ts", "../**/*.test.ts"],
|
||||
"include": ["../src/**/*", "*.js"]
|
||||
}
|
||||
|
@ -10,8 +10,9 @@ module.exports = {
|
||||
},
|
||||
coverageDirectory: '../../coverage/libs/ui',
|
||||
transform: {
|
||||
'^.+\\.(ts|js|html)$': 'jest-preset-angular'
|
||||
'^.+.(ts|mjs|js|html)$': 'jest-preset-angular'
|
||||
},
|
||||
transformIgnorePatterns: ['node_modules/(?!.*.mjs$)'],
|
||||
snapshotSerializers: [
|
||||
'jest-preset-angular/build/serializers/no-ng-attributes',
|
||||
'jest-preset-angular/build/serializers/ng-snapshot',
|
||||
|
@ -10,8 +10,6 @@ import {
|
||||
OnDestroy,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config';
|
||||
import { getBackgroundColor } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
Chart,
|
||||
Filler,
|
||||
@ -22,6 +20,11 @@ import {
|
||||
TimeScale
|
||||
} from 'chart.js';
|
||||
|
||||
import {
|
||||
primaryColorRgb,
|
||||
secondaryColorRgb
|
||||
} from '../../../../common/src/lib/config'; // TODO: @ghostfolio/common/config
|
||||
import { getBackgroundColor } from '../../../../common/src/lib/helper'; // TODO: @ghostfolio/common/helper
|
||||
import { LineChartItem } from './interfaces/line-chart.interface';
|
||||
|
||||
@Component({
|
||||
|
@ -1,7 +1,7 @@
|
||||
:host {
|
||||
.logo {
|
||||
background-color: rgba(var(--dark-primary-text));
|
||||
mask: url(~apps/client/src/assets/ghost.svg) no-repeat center;
|
||||
mask: url('/assets/ghost.svg') no-repeat center;
|
||||
}
|
||||
|
||||
.name {
|
||||
|
@ -6,6 +6,7 @@
|
||||
class="align-items-center justify-content-center"
|
||||
color="primary"
|
||||
[routerLink]="['/portfolio', 'transactions']"
|
||||
[queryParams]="{ createDialog: true }"
|
||||
mat-button
|
||||
>
|
||||
<span i18n>Time to add your first transaction.</span>
|
||||
|
@ -1,8 +1,11 @@
|
||||
:host {
|
||||
border: 1px solid rgba(var(--dark-dividers));
|
||||
border-radius: 0.25rem;
|
||||
display: block;
|
||||
|
||||
&.has-border {
|
||||
border: 1px solid rgba(var(--dark-dividers));
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
gf-logo {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { GfLogoModule } from '@ghostfolio/ui/logo';
|
||||
import { Meta, Story, moduleMetadata } from '@storybook/angular';
|
||||
|
||||
import { GfLogoModule } from '../../../../ui/src/lib/logo'; // TODO: @ghostfolio/ui/logo
|
||||
import { NoTransactionsInfoComponent } from './no-transactions-info.component';
|
||||
|
||||
export default {
|
||||
|
@ -1,4 +1,9 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
HostBinding,
|
||||
Input
|
||||
} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'gf-no-transactions-info-indicator',
|
||||
@ -6,8 +11,8 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
templateUrl: './no-transactions-info.component.html',
|
||||
styleUrls: ['./no-transactions-info.component.scss']
|
||||
})
|
||||
export class NoTransactionsInfoComponent implements OnInit {
|
||||
public constructor() {}
|
||||
export class NoTransactionsInfoComponent {
|
||||
@HostBinding('class.has-border') @Input() hasBorder = true;
|
||||
|
||||
public ngOnInit() {}
|
||||
public constructor() {}
|
||||
}
|
||||
|
@ -2,8 +2,8 @@ import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { GfLogoModule } from '@ghostfolio/ui/logo';
|
||||
|
||||
import { GfLogoModule } from '../../../../ui/src/lib/logo'; // TODO: @ghostfolio/ui/logo
|
||||
import { NoTransactionsInfoComponent } from './no-transactions-info.component';
|
||||
|
||||
@NgModule({
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Currency } from '@prisma/client';
|
||||
import { Meta, Story, moduleMetadata } from '@storybook/angular';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
|
||||
@ -24,7 +23,7 @@ const Template: Story<PortfolioProportionChartComponent> = (
|
||||
|
||||
export const Simple = Template.bind({});
|
||||
Simple.args = {
|
||||
baseCurrency: Currency.USD,
|
||||
baseCurrency: 'USD',
|
||||
keys: ['name'],
|
||||
locale: 'en-US',
|
||||
positions: {
|
||||
|
@ -8,8 +8,6 @@ import {
|
||||
OnDestroy,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||
import { getTextColor } from '@ghostfolio/common/helper';
|
||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||
import { Tooltip } from 'chart.js';
|
||||
import { LinearScale } from 'chart.js';
|
||||
@ -19,6 +17,9 @@ import { Chart } from 'chart.js';
|
||||
import ChartDataLabels from 'chartjs-plugin-datalabels';
|
||||
import * as Color from 'color';
|
||||
|
||||
import { UNKNOWN_KEY } from '../../../../common/src/lib/config'; // TODO: @ghostfolio/common/config
|
||||
import { getTextColor } from '../../../../common/src/lib/helper'; // TODO: @ghostfolio/common/helper
|
||||
|
||||
@Component({
|
||||
selector: 'gf-portfolio-proportion-chart',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
@ -53,13 +54,7 @@ export class PortfolioProportionChartComponent
|
||||
};
|
||||
|
||||
public constructor() {
|
||||
Chart.register(
|
||||
ArcElement,
|
||||
ChartDataLabels,
|
||||
DoughnutController,
|
||||
LinearScale,
|
||||
Tooltip
|
||||
);
|
||||
Chart.register(ArcElement, DoughnutController, LinearScale, Tooltip);
|
||||
}
|
||||
|
||||
public ngAfterViewInit() {
|
||||
@ -75,7 +70,6 @@ export class PortfolioProportionChartComponent
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
Chart.unregister(ChartDataLabels);
|
||||
this.chart?.destroy();
|
||||
}
|
||||
|
||||
@ -253,7 +247,7 @@ export class PortfolioProportionChartComponent
|
||||
} else {
|
||||
this.chart = new Chart(this.chartCanvas.nativeElement, {
|
||||
data,
|
||||
options: {
|
||||
options: <unknown>{
|
||||
cutout: '70%',
|
||||
layout: {
|
||||
padding: this.showLabels === true ? 100 : 0
|
||||
@ -315,6 +309,7 @@ export class PortfolioProportionChartComponent
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [ChartDataLabels],
|
||||
type: 'doughnut'
|
||||
});
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { DateRange } from '@ghostfolio/common/types';
|
||||
|
||||
import { MarketState } from '../../../../../apps/api/src/services/interfaces/interfaces'; // TODO: @ghostfolio/api/services/interfaces/interfaces
|
||||
import { DateRange } from '../../../../common/src/lib/types'; // TODO: @ghostfolio/common/types
|
||||
|
||||
@Component({
|
||||
selector: 'gf-trend-indicator',
|
||||
|
@ -8,10 +8,18 @@
|
||||
<div *ngIf="value > 0" class="mr-1 text-success">+</div>
|
||||
<div *ngIf="value < 0" class="mr-1 text-danger">-</div>
|
||||
</ng-container>
|
||||
<div *ngIf="isPercent" [ngClass]="size === 'medium' ? 'h4 mb-0' : ''">
|
||||
<div
|
||||
*ngIf="isPercent"
|
||||
class="mb-0"
|
||||
[ngClass]="{ h2: size === 'large', h4: size === 'medium' }"
|
||||
>
|
||||
{{ formattedValue }}%
|
||||
</div>
|
||||
<div *ngIf="!isPercent" [ngClass]="size === 'medium' ? 'h4 mb-0' : ''">
|
||||
<div
|
||||
*ngIf="!isPercent"
|
||||
class="mb-0"
|
||||
[ngClass]="{ h2: size === 'large', h4: size === 'medium' }"
|
||||
>
|
||||
<ng-container *ngIf="value === null">
|
||||
<span class="text-monospace text-muted">***</span>
|
||||
</ng-container>
|
||||
@ -27,7 +35,10 @@
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="isDate">
|
||||
<div [ngClass]="size === 'medium' ? 'h4 mb-0' : ''">
|
||||
<div
|
||||
class="mb-0"
|
||||
[ngClass]="{ h2: size === 'large', h4: size === 'medium' }"
|
||||
>
|
||||
{{ formattedDate }}
|
||||
</div>
|
||||
</ng-container>
|
||||
@ -41,7 +52,7 @@
|
||||
*ngIf="value === undefined"
|
||||
animation="pulse"
|
||||
[theme]="{
|
||||
height: '1.5rem',
|
||||
height: size === 'large' ? '2.5rem' : '1.5rem',
|
||||
width: '5rem'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
|
@ -4,10 +4,11 @@ import {
|
||||
Input,
|
||||
OnChanges
|
||||
} from '@angular/core';
|
||||
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
|
||||
import { format, isDate } from 'date-fns';
|
||||
import { isNumber } from 'lodash';
|
||||
|
||||
import { DEFAULT_DATE_FORMAT } from '../../../../common/src/lib/config'; // TODO: @ghostfolio/common/config
|
||||
|
||||
@Component({
|
||||
selector: 'gf-value',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
@ -23,7 +24,7 @@ export class ValueComponent implements OnChanges {
|
||||
@Input() locale = '';
|
||||
@Input() position = '';
|
||||
@Input() precision: number | undefined;
|
||||
@Input() size = '';
|
||||
@Input() size: 'large' | 'medium' | 'small' = 'small';
|
||||
@Input() value: number | string = '';
|
||||
|
||||
public absoluteValue = 0;
|
||||
|
@ -12,6 +12,7 @@
|
||||
"exclude": [
|
||||
"src/test-setup.ts",
|
||||
"**/*.spec.ts",
|
||||
"**/*.test.ts",
|
||||
"**/*.stories.ts",
|
||||
"**/*.stories.js"
|
||||
],
|
||||
|
@ -6,5 +6,5 @@
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"files": ["src/test-setup.ts"],
|
||||
"include": ["**/*.spec.ts", "**/*.d.ts"]
|
||||
"include": ["**/*.spec.ts", "**/*.test.ts", "**/*.d.ts"]
|
||||
}
|
||||
|
42
nx.json
42
nx.json
@ -23,32 +23,11 @@
|
||||
"test",
|
||||
"e2e",
|
||||
"build-storybook"
|
||||
]
|
||||
],
|
||||
"parallel": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"projects": {
|
||||
"api": {
|
||||
"tags": []
|
||||
},
|
||||
"client": {
|
||||
"tags": []
|
||||
},
|
||||
"client-e2e": {
|
||||
"tags": [],
|
||||
"implicitDependencies": ["client"]
|
||||
},
|
||||
"common": {
|
||||
"tags": []
|
||||
},
|
||||
"ui": {
|
||||
"tags": []
|
||||
},
|
||||
"ui-e2e": {
|
||||
"tags": [],
|
||||
"implicitDependencies": ["ui"]
|
||||
}
|
||||
},
|
||||
"targetDependencies": {
|
||||
"build": [
|
||||
{
|
||||
@ -56,5 +35,22 @@
|
||||
"projects": "dependencies"
|
||||
}
|
||||
]
|
||||
},
|
||||
"cli": {
|
||||
"defaultCollection": "@nrwl/nest"
|
||||
},
|
||||
"defaultProject": "api",
|
||||
"generators": {
|
||||
"@nrwl/angular:application": {
|
||||
"linter": "eslint",
|
||||
"unitTestRunner": "jest",
|
||||
"e2eTestRunner": "cypress"
|
||||
},
|
||||
"@nrwl/angular:library": {
|
||||
"linter": "eslint",
|
||||
"unitTestRunner": "jest"
|
||||
},
|
||||
"@nrwl/nest": {},
|
||||
"@nrwl/angular:component": {}
|
||||
}
|
||||
}
|
||||
|
112
package.json
112
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ghostfolio",
|
||||
"version": "1.72.0",
|
||||
"version": "1.81.0",
|
||||
"homepage": "https://ghostfol.io",
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@ -48,27 +48,27 @@
|
||||
"workspace-generator": "nx workspace-generator"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "12.2.4",
|
||||
"@angular/cdk": "12.2.4",
|
||||
"@angular/common": "12.2.4",
|
||||
"@angular/compiler": "12.2.4",
|
||||
"@angular/core": "12.2.4",
|
||||
"@angular/forms": "12.2.4",
|
||||
"@angular/material": "12.2.4",
|
||||
"@angular/platform-browser": "12.2.4",
|
||||
"@angular/platform-browser-dynamic": "12.2.4",
|
||||
"@angular/router": "12.2.4",
|
||||
"@angular/animations": "13.0.2",
|
||||
"@angular/cdk": "13.0.2",
|
||||
"@angular/common": "13.0.2",
|
||||
"@angular/compiler": "13.0.2",
|
||||
"@angular/core": "13.0.2",
|
||||
"@angular/forms": "13.0.2",
|
||||
"@angular/material": "13.0.2",
|
||||
"@angular/platform-browser": "13.0.2",
|
||||
"@angular/platform-browser-dynamic": "13.0.2",
|
||||
"@angular/router": "13.0.2",
|
||||
"@codewithdan/observable-store": "2.2.11",
|
||||
"@dinero.js/currencies": "2.0.0-alpha.8",
|
||||
"@nestjs/common": "7.6.18",
|
||||
"@nestjs/config": "0.6.3",
|
||||
"@nestjs/core": "7.6.18",
|
||||
"@nestjs/jwt": "7.2.0",
|
||||
"@nestjs/passport": "7.1.6",
|
||||
"@nestjs/platform-express": "7.6.18",
|
||||
"@nestjs/schedule": "0.4.3",
|
||||
"@nestjs/serve-static": "2.1.4",
|
||||
"@nrwl/angular": "12.8.0",
|
||||
"@nestjs/common": "8.2.3",
|
||||
"@nestjs/config": "1.1.3",
|
||||
"@nestjs/core": "8.2.3",
|
||||
"@nestjs/jwt": "8.0.0",
|
||||
"@nestjs/passport": "8.0.1",
|
||||
"@nestjs/platform-express": "8.2.3",
|
||||
"@nestjs/schedule": "1.0.2",
|
||||
"@nestjs/serve-static": "2.2.2",
|
||||
"@nrwl/angular": "13.2.2",
|
||||
"@prisma/client": "2.30.2",
|
||||
"@simplewebauthn/browser": "4.1.0",
|
||||
"@simplewebauthn/server": "4.1.0",
|
||||
@ -76,7 +76,7 @@
|
||||
"@stripe/stripe-js": "1.15.0",
|
||||
"@types/papaparse": "5.2.6",
|
||||
"alphavantage": "2.2.0",
|
||||
"angular-material-css-vars": "2.1.2",
|
||||
"angular-material-css-vars": "3.0.0",
|
||||
"bent": "7.3.12",
|
||||
"big.js": "6.1.1",
|
||||
"bootstrap": "4.6.0",
|
||||
@ -108,7 +108,7 @@
|
||||
"prisma": "2.30.2",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"round-to": "5.0.0",
|
||||
"rxjs": "6.6.7",
|
||||
"rxjs": "7.4.0",
|
||||
"stripe": "8.156.0",
|
||||
"svgmap": "2.6.0",
|
||||
"tslib": "2.0.0",
|
||||
@ -117,56 +117,56 @@
|
||||
"zone.js": "0.11.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "12.2.4",
|
||||
"@angular-eslint/eslint-plugin": "12.3.1",
|
||||
"@angular-eslint/eslint-plugin-template": "12.3.0",
|
||||
"@angular-eslint/template-parser": "12.3.0",
|
||||
"@angular/cli": "12.2.4",
|
||||
"@angular/compiler-cli": "12.2.4",
|
||||
"@angular/language-service": "12.2.4",
|
||||
"@angular/localize": "12.2.4",
|
||||
"@nestjs/schematics": "7.3.1",
|
||||
"@nestjs/testing": "7.6.18",
|
||||
"@nrwl/cli": "12.8.0",
|
||||
"@nrwl/cypress": "12.8.0",
|
||||
"@nrwl/eslint-plugin-nx": "12.8.0",
|
||||
"@nrwl/jest": "12.8.0",
|
||||
"@nrwl/nest": "12.8.0",
|
||||
"@nrwl/node": "12.8.0",
|
||||
"@nrwl/storybook": "12.9.0",
|
||||
"@nrwl/tao": "12.8.0",
|
||||
"@nrwl/workspace": "12.8.0",
|
||||
"@storybook/addon-essentials": "6.3.8",
|
||||
"@storybook/angular": "6.3.8",
|
||||
"@storybook/builder-webpack5": "6.3.8",
|
||||
"@storybook/manager-webpack5": "6.3.8",
|
||||
"@angular-devkit/build-angular": "13.0.3",
|
||||
"@angular-eslint/eslint-plugin": "13.0.1",
|
||||
"@angular-eslint/eslint-plugin-template": "13.0.1",
|
||||
"@angular-eslint/template-parser": "13.0.1",
|
||||
"@angular/cli": "13.0.3",
|
||||
"@angular/compiler-cli": "13.0.2",
|
||||
"@angular/language-service": "13.0.2",
|
||||
"@angular/localize": "13.0.2",
|
||||
"@nestjs/schematics": "8.0.5",
|
||||
"@nestjs/testing": "8.2.3",
|
||||
"@nrwl/cli": "13.2.2",
|
||||
"@nrwl/cypress": "13.2.2",
|
||||
"@nrwl/eslint-plugin-nx": "13.2.2",
|
||||
"@nrwl/jest": "13.2.2",
|
||||
"@nrwl/nest": "13.2.2",
|
||||
"@nrwl/node": "13.2.2",
|
||||
"@nrwl/storybook": "13.2.2",
|
||||
"@nrwl/tao": "13.2.2",
|
||||
"@nrwl/workspace": "13.2.2",
|
||||
"@storybook/addon-essentials": "6.4.0-rc.3",
|
||||
"@storybook/angular": "6.4.0-rc.3",
|
||||
"@storybook/builder-webpack5": "6.4.0-rc.3",
|
||||
"@storybook/manager-webpack5": "6.4.0-rc.3",
|
||||
"@types/big.js": "6.1.2",
|
||||
"@types/cache-manager": "3.4.2",
|
||||
"@types/color": "3.0.2",
|
||||
"@types/jest": "26.0.20",
|
||||
"@types/jest": "27.0.2",
|
||||
"@types/lodash": "4.14.174",
|
||||
"@types/node": "14.14.33",
|
||||
"@types/passport-google-oauth20": "2.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "4.27.0",
|
||||
"@typescript-eslint/parser": "4.27.0",
|
||||
"@types/passport-google-oauth20": "2.0.11",
|
||||
"@typescript-eslint/eslint-plugin": "5.4.0",
|
||||
"@typescript-eslint/parser": "5.4.0",
|
||||
"codelyzer": "6.0.1",
|
||||
"cypress": "6.2.1",
|
||||
"dotenv": "10.0.0",
|
||||
"eslint": "7.28.0",
|
||||
"eslint": "8.3.0",
|
||||
"eslint-config-prettier": "8.3.0",
|
||||
"eslint-plugin-cypress": "2.10.3",
|
||||
"eslint-plugin-import": "2.23.4",
|
||||
"eslint-plugin-cypress": "2.12.1",
|
||||
"eslint-plugin-import": "2.25.3",
|
||||
"import-sort-cli": "6.0.0",
|
||||
"import-sort-parser-typescript": "6.0.0",
|
||||
"import-sort-style-module": "6.0.0",
|
||||
"jest": "27.0.3",
|
||||
"jest-preset-angular": "9.0.3",
|
||||
"jest": "27.2.3",
|
||||
"jest-preset-angular": "11.0.0",
|
||||
"prettier": "2.3.2",
|
||||
"replace-in-file": "6.2.0",
|
||||
"rimraf": "3.0.2",
|
||||
"ts-jest": "27.0.3",
|
||||
"ts-jest": "27.0.5",
|
||||
"ts-node": "9.1.1",
|
||||
"typescript": "4.3.5"
|
||||
"typescript": "4.4.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
|
Reference in New Issue
Block a user