Compare commits
22 Commits
Author | SHA1 | Date | |
---|---|---|---|
a83441b3ba | |||
075431d868 | |||
0168c1c4e8 | |||
07de8f87fc | |||
3e16041c16 | |||
5882b7914d | |||
69c9e259b1 | |||
aca37a27f9 | |||
313d2a2f79 | |||
9ac67b0af2 | |||
1e526852a7 | |||
e54638a684 | |||
0179823ad9 | |||
029b7bed9a | |||
635f10e2d0 | |||
cebf879d67 | |||
124bdc028d | |||
d69a69ce18 | |||
15344513ce | |||
b291d9e031 | |||
bee702302f | |||
bb56e09a13 |
50
CHANGELOG.md
50
CHANGELOG.md
@ -5,6 +5,56 @@ 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.101.0 - 08.01.2022
|
||||
|
||||
### Added
|
||||
|
||||
- Added `GOOGLE_SHEETS` as a new data source type
|
||||
|
||||
### Changed
|
||||
|
||||
- Excluded the url pattern of shared portfolios in the `robots.txt` file
|
||||
|
||||
### Todo
|
||||
|
||||
- Apply data migration (`yarn database:migrate`)
|
||||
|
||||
## 1.100.0 - 05.01.2022
|
||||
|
||||
### Added
|
||||
|
||||
- Added the _Top 3_ and _Bottom 3_ performers to the analysis page
|
||||
- Added a blog post
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the routing of the create activity dialog
|
||||
- Fixed the link color in the blog posts
|
||||
|
||||
## 1.99.0 - 01.01.2022
|
||||
|
||||
### Added
|
||||
|
||||
- Exposed the profile data gathering by symbol as an endpoint
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the portfolio analysis page: show the y-axis and extend the chart in relation to the days in market
|
||||
- Restructured the about page
|
||||
- Start refactoring _transactions_ to _activities_
|
||||
- Refactored the demo user id
|
||||
- Upgraded `angular` from version `13.0.2` to `13.1.1`
|
||||
- Upgraded `chart.js` from version `3.5.0` to `3.7.0`
|
||||
- Upgraded `Nx` from version `13.3.0` to `13.4.1`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Hid the data provider warning while loading
|
||||
- Fixed an exception with the market state caused by a failed data provider request
|
||||
- Fixed an exception in the portfolio position endpoint
|
||||
- Fixed the reload of the position detail dialog (with query parameters)
|
||||
- Fixed the missing mapping for Russia in the data enhancer for symbol profile data via _Trackinsight_
|
||||
|
||||
## 1.98.0 - 29.12.2021
|
||||
|
||||
### Added
|
||||
|
@ -96,6 +96,29 @@ export class AdminController {
|
||||
return;
|
||||
}
|
||||
|
||||
@Post('gather/profile-data/:dataSource/:symbol')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async gatherProfileDataForSymbol(
|
||||
@Param('dataSource') dataSource: DataSource,
|
||||
@Param('symbol') symbol: string
|
||||
): Promise<void> {
|
||||
if (
|
||||
!hasPermission(
|
||||
this.request.user.permissions,
|
||||
permissions.accessAdminControl
|
||||
)
|
||||
) {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||
StatusCodes.FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
this.dataGatheringService.gatherProfileData([{ dataSource, symbol }]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@Post('gather/:dataSource/:symbol')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async gatherSymbol(
|
||||
|
@ -6,6 +6,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||
import {
|
||||
DEMO_USER_ID,
|
||||
PROPERTY_IS_READ_ONLY_MODE,
|
||||
PROPERTY_SLACK_COMMUNITY_USERS,
|
||||
PROPERTY_STRIPE_CONFIG,
|
||||
@ -23,7 +24,6 @@ import { subDays } from 'date-fns';
|
||||
@Injectable()
|
||||
export class InfoService {
|
||||
private static CACHE_KEY_STATISTICS = 'STATISTICS';
|
||||
private static DEMO_USER_ID = '9b112b4d-3b7d-4bad-9bdd-3b0f7b4dac2f';
|
||||
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService,
|
||||
@ -196,7 +196,7 @@ export class InfoService {
|
||||
|
||||
private getDemoAuthToken() {
|
||||
return this.jwtService.sign({
|
||||
id: InfoService.DEMO_USER_ID
|
||||
id: DEMO_USER_ID
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { parseDate, resetHours } from '@ghostfolio/common/helper';
|
||||
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
|
||||
import { DataSource } from '@prisma/client';
|
||||
import Big from 'big.js';
|
||||
import {
|
||||
addDays,
|
||||
differenceInCalendarDays,
|
||||
endOfDay,
|
||||
format,
|
||||
isBefore,
|
||||
isSameDay
|
||||
} from 'date-fns';
|
||||
@ -67,15 +68,202 @@ function mockGetValue(symbol: string, date: Date) {
|
||||
|
||||
return { marketPrice: 0 };
|
||||
case 'VTI':
|
||||
return {
|
||||
marketPrice: new Big('144.38')
|
||||
.plus(
|
||||
new Big('0.08').mul(
|
||||
differenceInCalendarDays(date, parseDate('2019-02-01'))
|
||||
)
|
||||
)
|
||||
.toNumber()
|
||||
};
|
||||
switch (format(date, DATE_FORMAT)) {
|
||||
case '2019-01-01':
|
||||
return { marketPrice: 144.38 };
|
||||
case '2019-02-01':
|
||||
return { marketPrice: 144.38 };
|
||||
case '2019-03-01':
|
||||
return { marketPrice: 146.62 };
|
||||
case '2019-04-01':
|
||||
return { marketPrice: 149.1 };
|
||||
case '2019-05-01':
|
||||
return { marketPrice: 151.5 };
|
||||
case '2019-06-01':
|
||||
return { marketPrice: 153.98 };
|
||||
case '2019-07-01':
|
||||
return { marketPrice: 156.38 };
|
||||
case '2019-08-01':
|
||||
return { marketPrice: 158.86 };
|
||||
case '2019-08-03':
|
||||
return { marketPrice: 159.02 };
|
||||
case '2019-09-01':
|
||||
return { marketPrice: 161.34 };
|
||||
case '2019-10-01':
|
||||
return { marketPrice: 163.74 };
|
||||
case '2019-11-01':
|
||||
return { marketPrice: 166.22 };
|
||||
case '2019-12-01':
|
||||
return { marketPrice: 168.62 };
|
||||
case '2020-01-01':
|
||||
return { marketPrice: 171.1 };
|
||||
case '2020-02-01':
|
||||
return { marketPrice: 173.58 };
|
||||
case '2020-02-02':
|
||||
return { marketPrice: 173.66 };
|
||||
case '2020-03-01':
|
||||
return { marketPrice: 175.9 };
|
||||
case '2020-04-01':
|
||||
return { marketPrice: 178.38 };
|
||||
case '2020-05-01':
|
||||
return { marketPrice: 180.78 };
|
||||
case '2020-06-01':
|
||||
return { marketPrice: 183.26 };
|
||||
case '2020-07-01':
|
||||
return { marketPrice: 185.66 };
|
||||
case '2020-08-01':
|
||||
return { marketPrice: 188.14 };
|
||||
case '2020-08-02':
|
||||
return { marketPrice: 188.22 };
|
||||
case '2020-08-03':
|
||||
return { marketPrice: 188.3 };
|
||||
case '2020-09-01':
|
||||
return { marketPrice: 190.62 };
|
||||
case '2020-10-01':
|
||||
return { marketPrice: 193.02 };
|
||||
case '2020-11-01':
|
||||
return { marketPrice: 195.5 };
|
||||
case '2020-12-01':
|
||||
return { marketPrice: 197.9 };
|
||||
case '2021-01-01':
|
||||
return { marketPrice: 200.38 };
|
||||
case '2021-02-01':
|
||||
return { marketPrice: 202.86 };
|
||||
case '2021-03-01':
|
||||
return { marketPrice: 205.1 };
|
||||
case '2021-04-01':
|
||||
return { marketPrice: 207.58 };
|
||||
case '2021-05-01':
|
||||
return { marketPrice: 209.98 };
|
||||
case '2021-06-01':
|
||||
return { marketPrice: 212.46 };
|
||||
case '2021-06-02':
|
||||
return { marketPrice: 212.54 };
|
||||
case '2021-06-03':
|
||||
return { marketPrice: 212.62 };
|
||||
case '2021-06-04':
|
||||
return { marketPrice: 212.7 };
|
||||
case '2021-06-05':
|
||||
return { marketPrice: 212.78 };
|
||||
case '2021-06-06':
|
||||
return { marketPrice: 212.86 };
|
||||
case '2021-06-07':
|
||||
return { marketPrice: 212.94 };
|
||||
case '2021-06-08':
|
||||
return { marketPrice: 213.02 };
|
||||
case '2021-06-09':
|
||||
return { marketPrice: 213.1 };
|
||||
case '2021-06-10':
|
||||
return { marketPrice: 213.18 };
|
||||
case '2021-06-11':
|
||||
return { marketPrice: 213.26 };
|
||||
case '2021-06-12':
|
||||
return { marketPrice: 213.34 };
|
||||
case '2021-06-13':
|
||||
return { marketPrice: 213.42 };
|
||||
case '2021-06-14':
|
||||
return { marketPrice: 213.5 };
|
||||
case '2021-06-15':
|
||||
return { marketPrice: 213.58 };
|
||||
case '2021-06-16':
|
||||
return { marketPrice: 213.66 };
|
||||
case '2021-06-17':
|
||||
return { marketPrice: 213.74 };
|
||||
case '2021-06-18':
|
||||
return { marketPrice: 213.82 };
|
||||
case '2021-06-19':
|
||||
return { marketPrice: 213.9 };
|
||||
case '2021-06-20':
|
||||
return { marketPrice: 213.98 };
|
||||
case '2021-06-21':
|
||||
return { marketPrice: 214.06 };
|
||||
case '2021-06-22':
|
||||
return { marketPrice: 214.14 };
|
||||
case '2021-06-23':
|
||||
return { marketPrice: 214.22 };
|
||||
case '2021-06-24':
|
||||
return { marketPrice: 214.3 };
|
||||
case '2021-06-25':
|
||||
return { marketPrice: 214.38 };
|
||||
case '2021-06-26':
|
||||
return { marketPrice: 214.46 };
|
||||
case '2021-06-27':
|
||||
return { marketPrice: 214.54 };
|
||||
case '2021-06-28':
|
||||
return { marketPrice: 214.62 };
|
||||
case '2021-06-29':
|
||||
return { marketPrice: 214.7 };
|
||||
case '2021-06-30':
|
||||
return { marketPrice: 214.78 };
|
||||
case '2021-07-01':
|
||||
return { marketPrice: 214.86 };
|
||||
case '2021-07-02':
|
||||
return { marketPrice: 214.94 };
|
||||
case '2021-07-03':
|
||||
return { marketPrice: 215.02 };
|
||||
case '2021-07-04':
|
||||
return { marketPrice: 215.1 };
|
||||
case '2021-07-05':
|
||||
return { marketPrice: 215.18 };
|
||||
case '2021-07-06':
|
||||
return { marketPrice: 215.26 };
|
||||
case '2021-07-07':
|
||||
return { marketPrice: 215.34 };
|
||||
case '2021-07-08':
|
||||
return { marketPrice: 215.42 };
|
||||
case '2021-07-09':
|
||||
return { marketPrice: 215.5 };
|
||||
case '2021-07-10':
|
||||
return { marketPrice: 215.58 };
|
||||
case '2021-07-11':
|
||||
return { marketPrice: 215.66 };
|
||||
case '2021-07-12':
|
||||
return { marketPrice: 215.74 };
|
||||
case '2021-07-13':
|
||||
return { marketPrice: 215.82 };
|
||||
case '2021-07-14':
|
||||
return { marketPrice: 215.9 };
|
||||
case '2021-07-15':
|
||||
return { marketPrice: 215.98 };
|
||||
case '2021-07-16':
|
||||
return { marketPrice: 216.06 };
|
||||
case '2021-07-17':
|
||||
return { marketPrice: 216.14 };
|
||||
case '2021-07-18':
|
||||
return { marketPrice: 216.22 };
|
||||
case '2021-07-19':
|
||||
return { marketPrice: 216.3 };
|
||||
case '2021-07-20':
|
||||
return { marketPrice: 216.38 };
|
||||
case '2021-07-21':
|
||||
return { marketPrice: 216.46 };
|
||||
case '2021-07-22':
|
||||
return { marketPrice: 216.54 };
|
||||
case '2021-07-23':
|
||||
return { marketPrice: 216.62 };
|
||||
case '2021-07-24':
|
||||
return { marketPrice: 216.7 };
|
||||
case '2021-07-25':
|
||||
return { marketPrice: 216.78 };
|
||||
case '2021-07-26':
|
||||
return { marketPrice: 216.86 };
|
||||
case '2021-07-27':
|
||||
return { marketPrice: 216.94 };
|
||||
case '2021-07-28':
|
||||
return { marketPrice: 217.02 };
|
||||
case '2021-07-29':
|
||||
return { marketPrice: 217.1 };
|
||||
case '2021-07-30':
|
||||
return { marketPrice: 217.18 };
|
||||
case '2021-07-31':
|
||||
return { marketPrice: 217.26 };
|
||||
case '2021-08-01':
|
||||
return { marketPrice: 217.34 };
|
||||
case '2020-10-24':
|
||||
return { marketPrice: 194.86 };
|
||||
default:
|
||||
return { marketPrice: 0 };
|
||||
}
|
||||
|
||||
default:
|
||||
return { marketPrice: 0 };
|
||||
@ -1645,14 +1833,14 @@ describe('PortfolioCalculator', () => {
|
||||
grossPerformance: new Big('498.3'),
|
||||
netPerformance: new Big('498.3'),
|
||||
investment: new Big('2923.7'),
|
||||
value: new Big('3422') // 20 * (144.38 + days=335 * 0.08)
|
||||
value: new Big('3422') // 20 * 171.1
|
||||
},
|
||||
{
|
||||
date: '2021-01-01',
|
||||
grossPerformance: new Big('349.35'),
|
||||
netPerformance: new Big('349.35'),
|
||||
investment: new Big('652.55'),
|
||||
value: new Big('1001.9') // 5 * (144.38 + days=700 * 0.08)
|
||||
value: new Big('1001.9') // 5 * 200.38
|
||||
}
|
||||
]);
|
||||
});
|
||||
@ -1765,14 +1953,14 @@ describe('PortfolioCalculator', () => {
|
||||
grossPerformance: new Big('498.3'),
|
||||
netPerformance: new Big('398.3'), // 100 fees
|
||||
investment: new Big('2923.7'),
|
||||
value: new Big('3422') // 20 * (144.38 + days=335 * 0.08)
|
||||
value: new Big('3422') // 20 * 171.1
|
||||
},
|
||||
{
|
||||
date: '2021-01-01',
|
||||
grossPerformance: new Big('349.35'),
|
||||
netPerformance: new Big('199.35'), // 150 fees
|
||||
investment: new Big('652.55'),
|
||||
value: new Big('1001.9') // 5 * (144.38 + days=700 * 0.08)
|
||||
value: new Big('1001.9') // 5 * 200.38
|
||||
}
|
||||
]);
|
||||
});
|
||||
@ -1808,203 +1996,203 @@ describe('PortfolioCalculator', () => {
|
||||
grossPerformance: new Big('0'),
|
||||
netPerformance: new Big('0'),
|
||||
investment: new Big('1443.8'),
|
||||
value: new Big('1443.8') // 10 * (144.38 + days=0 * 0.08)
|
||||
value: new Big('1443.8') // 10 * 144.38
|
||||
},
|
||||
{
|
||||
date: '2019-03-01',
|
||||
grossPerformance: new Big('22.4'),
|
||||
netPerformance: new Big('22.4'),
|
||||
investment: new Big('1443.8'),
|
||||
value: new Big('1466.2') // 10 * (144.38 + days=28 * 0.08)
|
||||
value: new Big('1466.2') // 10 * 146.62
|
||||
},
|
||||
{
|
||||
date: '2019-04-01',
|
||||
grossPerformance: new Big('47.2'),
|
||||
netPerformance: new Big('47.2'),
|
||||
investment: new Big('1443.8'),
|
||||
value: new Big('1491') // 10 * (144.38 + days=59 * 0.08)
|
||||
value: new Big('1491') // 10 * 149.1
|
||||
},
|
||||
{
|
||||
date: '2019-05-01',
|
||||
grossPerformance: new Big('71.2'),
|
||||
netPerformance: new Big('71.2'),
|
||||
investment: new Big('1443.8'),
|
||||
value: new Big('1515') // 10 * (144.38 + days=89 * 0.08)
|
||||
value: new Big('1515') // 10 * 151.5
|
||||
},
|
||||
{
|
||||
date: '2019-06-01',
|
||||
grossPerformance: new Big('96'),
|
||||
netPerformance: new Big('96'),
|
||||
investment: new Big('1443.8'),
|
||||
value: new Big('1539.8') // 10 * (144.38 + days=120 * 0.08)
|
||||
value: new Big('1539.8') // 10 * 153.98
|
||||
},
|
||||
{
|
||||
date: '2019-07-01',
|
||||
grossPerformance: new Big('120'),
|
||||
netPerformance: new Big('120'),
|
||||
investment: new Big('1443.8'),
|
||||
value: new Big('1563.8') // 10 * (144.38 + days=150 * 0.08)
|
||||
value: new Big('1563.8') // 10 * 156.38
|
||||
},
|
||||
{
|
||||
date: '2019-08-01',
|
||||
grossPerformance: new Big('144.8'),
|
||||
netPerformance: new Big('144.8'),
|
||||
investment: new Big('1443.8'),
|
||||
value: new Big('1588.6') // 10 * (144.38 + days=181 * 0.08)
|
||||
value: new Big('1588.6') // 10 * 158.86
|
||||
},
|
||||
{
|
||||
date: '2019-09-01',
|
||||
grossPerformance: new Big('303.1'),
|
||||
netPerformance: new Big('303.1'),
|
||||
investment: new Big('2923.7'),
|
||||
value: new Big('3226.8') // 20 * (144.38 + days=212 * 0.08)
|
||||
value: new Big('3226.8') // 20 * 161.34
|
||||
},
|
||||
{
|
||||
date: '2019-10-01',
|
||||
grossPerformance: new Big('351.1'),
|
||||
netPerformance: new Big('351.1'),
|
||||
investment: new Big('2923.7'),
|
||||
value: new Big('3274.8') // 20 * (144.38 + days=242 * 0.08)
|
||||
value: new Big('3274.8') // 20 * 163.74
|
||||
},
|
||||
{
|
||||
date: '2019-11-01',
|
||||
grossPerformance: new Big('400.7'),
|
||||
netPerformance: new Big('400.7'),
|
||||
investment: new Big('2923.7'),
|
||||
value: new Big('3324.4') // 20 * (144.38 + days=273 * 0.08)
|
||||
value: new Big('3324.4') // 20 * 166.22
|
||||
},
|
||||
{
|
||||
date: '2019-12-01',
|
||||
grossPerformance: new Big('448.7'),
|
||||
netPerformance: new Big('448.7'),
|
||||
investment: new Big('2923.7'),
|
||||
value: new Big('3372.4') // 20 * (144.38 + days=303 * 0.08)
|
||||
value: new Big('3372.4') // 20 * 168.62
|
||||
},
|
||||
{
|
||||
date: '2020-01-01',
|
||||
grossPerformance: new Big('498.3'),
|
||||
netPerformance: new Big('498.3'),
|
||||
investment: new Big('2923.7'),
|
||||
value: new Big('3422') // 20 * (144.38 + days=335 * 0.08)
|
||||
value: new Big('3422') // 20 * 171.1
|
||||
},
|
||||
{
|
||||
date: '2020-02-01',
|
||||
grossPerformance: new Big('547.9'),
|
||||
netPerformance: new Big('547.9'),
|
||||
investment: new Big('2923.7'),
|
||||
value: new Big('3471.6') // 20 * (144.38 + days=365 * 0.08)
|
||||
value: new Big('3471.6') // 20 * 173.58
|
||||
},
|
||||
{
|
||||
date: '2020-03-01',
|
||||
grossPerformance: new Big('226.95'),
|
||||
netPerformance: new Big('226.95'),
|
||||
investment: new Big('652.55'),
|
||||
value: new Big('879.5') // 5 * (144.38 + days=394 * 0.08)
|
||||
value: new Big('879.5') // 5 * 175.9
|
||||
},
|
||||
{
|
||||
date: '2020-04-01',
|
||||
grossPerformance: new Big('239.35'),
|
||||
netPerformance: new Big('239.35'),
|
||||
investment: new Big('652.55'),
|
||||
value: new Big('891.9') // 5 * (144.38 + days=425 * 0.08)
|
||||
value: new Big('891.9') // 5 * 178.38
|
||||
},
|
||||
{
|
||||
date: '2020-05-01',
|
||||
grossPerformance: new Big('251.35'),
|
||||
netPerformance: new Big('251.35'),
|
||||
investment: new Big('652.55'),
|
||||
value: new Big('903.9') // 5 * (144.38 + days=455 * 0.08)
|
||||
value: new Big('903.9') // 5 * 180.78
|
||||
},
|
||||
{
|
||||
date: '2020-06-01',
|
||||
grossPerformance: new Big('263.75'),
|
||||
netPerformance: new Big('263.75'),
|
||||
investment: new Big('652.55'),
|
||||
value: new Big('916.3') // 5 * (144.38 + days=486 * 0.08)
|
||||
value: new Big('916.3') // 5 * 183.26
|
||||
},
|
||||
{
|
||||
date: '2020-07-01',
|
||||
grossPerformance: new Big('275.75'),
|
||||
netPerformance: new Big('275.75'),
|
||||
investment: new Big('652.55'),
|
||||
value: new Big('928.3') // 5 * (144.38 + days=516 * 0.08)
|
||||
value: new Big('928.3') // 5 * 185.66
|
||||
},
|
||||
{
|
||||
date: '2020-08-01',
|
||||
grossPerformance: new Big('288.15'),
|
||||
netPerformance: new Big('288.15'),
|
||||
investment: new Big('652.55'),
|
||||
value: new Big('940.7') // 5 * (144.38 + days=547 * 0.08)
|
||||
value: new Big('940.7') // 5 * 188.14
|
||||
},
|
||||
{
|
||||
date: '2020-09-01',
|
||||
grossPerformance: new Big('300.55'),
|
||||
netPerformance: new Big('300.55'),
|
||||
investment: new Big('652.55'),
|
||||
value: new Big('953.1') // 5 * (144.38 + days=578 * 0.08)
|
||||
value: new Big('953.1') // 5 * 190.62
|
||||
},
|
||||
{
|
||||
date: '2020-10-01',
|
||||
grossPerformance: new Big('312.55'),
|
||||
netPerformance: new Big('312.55'),
|
||||
investment: new Big('652.55'),
|
||||
value: new Big('965.1') // 5 * (144.38 + days=608 * 0.08)
|
||||
value: new Big('965.1') // 5 * 193.02
|
||||
},
|
||||
{
|
||||
date: '2020-11-01',
|
||||
grossPerformance: new Big('324.95'),
|
||||
netPerformance: new Big('324.95'),
|
||||
investment: new Big('652.55'),
|
||||
value: new Big('977.5') // 5 * (144.38 + days=639 * 0.08)
|
||||
value: new Big('977.5') // 5 * 195.5
|
||||
},
|
||||
{
|
||||
date: '2020-12-01',
|
||||
grossPerformance: new Big('336.95'),
|
||||
netPerformance: new Big('336.95'),
|
||||
investment: new Big('652.55'),
|
||||
value: new Big('989.5') // 5 * (144.38 + days=669 * 0.08)
|
||||
value: new Big('989.5') // 5 * 197.9
|
||||
},
|
||||
{
|
||||
date: '2021-01-01',
|
||||
grossPerformance: new Big('349.35'),
|
||||
netPerformance: new Big('349.35'),
|
||||
investment: new Big('652.55'),
|
||||
value: new Big('1001.9') // 5 * (144.38 + days=700 * 0.08)
|
||||
value: new Big('1001.9') // 5 * 200.38
|
||||
},
|
||||
{
|
||||
date: '2021-02-01',
|
||||
grossPerformance: new Big('358.85'),
|
||||
netPerformance: new Big('358.85'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3042.9') // 15 * (144.38 + days=731 * 0.08)
|
||||
value: new Big('3042.9') // 15 * 202.86
|
||||
},
|
||||
{
|
||||
date: '2021-03-01',
|
||||
grossPerformance: new Big('392.45'),
|
||||
netPerformance: new Big('392.45'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3076.5') // 15 * (144.38 + days=759 * 0.08)
|
||||
value: new Big('3076.5') // 15 * 205.1
|
||||
},
|
||||
{
|
||||
date: '2021-04-01',
|
||||
grossPerformance: new Big('429.65'),
|
||||
netPerformance: new Big('429.65'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3113.7') // 15 * (144.38 + days=790 * 0.08)
|
||||
value: new Big('3113.7') // 15 * 207.58
|
||||
},
|
||||
{
|
||||
date: '2021-05-01',
|
||||
grossPerformance: new Big('465.65'),
|
||||
netPerformance: new Big('465.65'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3149.7') // 15 * (144.38 + days=820 * 0.08)
|
||||
value: new Big('3149.7') // 15 * 209.98
|
||||
},
|
||||
{
|
||||
date: '2021-06-01',
|
||||
grossPerformance: new Big('502.85'),
|
||||
netPerformance: new Big('502.85'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3186.9') // 15 * (144.38 + days=851 * 0.08)
|
||||
value: new Big('3186.9') // 15 * 212.46
|
||||
}
|
||||
]);
|
||||
|
||||
@ -2047,49 +2235,49 @@ describe('PortfolioCalculator', () => {
|
||||
grossPerformance: new Big('498.3'),
|
||||
netPerformance: new Big('498.3'),
|
||||
investment: new Big('2923.7'),
|
||||
value: new Big('3422') // 20 * (144.38 + days=335 * 0.08)
|
||||
value: new Big('3422') // 20 * 171.1
|
||||
},
|
||||
{
|
||||
date: '2021-01-01',
|
||||
grossPerformance: new Big('349.35'),
|
||||
netPerformance: new Big('349.35'),
|
||||
investment: new Big('652.55'),
|
||||
value: new Big('1001.9') // 5 * (144.38 + days=700 * 0.08)
|
||||
value: new Big('1001.9') // 5 * 200.38
|
||||
},
|
||||
{
|
||||
date: '2021-02-01',
|
||||
grossPerformance: new Big('358.85'),
|
||||
netPerformance: new Big('358.85'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3042.9') // 15 * (144.38 + days=731 * 0.08)
|
||||
value: new Big('3042.9') // 15 * 202.86
|
||||
},
|
||||
{
|
||||
date: '2021-03-01',
|
||||
grossPerformance: new Big('392.45'),
|
||||
netPerformance: new Big('392.45'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3076.5') // 15 * (144.38 + days=759 * 0.08)
|
||||
value: new Big('3076.5') // 15 * 205.1
|
||||
},
|
||||
{
|
||||
date: '2021-04-01',
|
||||
grossPerformance: new Big('429.65'),
|
||||
netPerformance: new Big('429.65'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3113.7') // 15 * (144.38 + days=790 * 0.08)
|
||||
value: new Big('3113.7') // 15 * 207.58
|
||||
},
|
||||
{
|
||||
date: '2021-05-01',
|
||||
grossPerformance: new Big('465.65'),
|
||||
netPerformance: new Big('465.65'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3149.7') // 15 * (144.38 + days=820 * 0.08)
|
||||
value: new Big('3149.7') // 15 * 209.98
|
||||
},
|
||||
{
|
||||
date: '2021-06-01',
|
||||
grossPerformance: new Big('502.85'),
|
||||
netPerformance: new Big('502.85'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3186.9') // 15 * (144.38 + days=851 * 0.08)
|
||||
value: new Big('3186.9') // 15 * 212.46
|
||||
}
|
||||
]);
|
||||
});
|
||||
@ -2134,252 +2322,252 @@ describe('PortfolioCalculator', () => {
|
||||
grossPerformance: new Big('498.3'),
|
||||
netPerformance: new Big('498.3'),
|
||||
investment: new Big('2923.7'),
|
||||
value: new Big('3422') // 20 * (144.38 + days=335 * 0.08)
|
||||
value: new Big('3422') // 20 * 171.1
|
||||
},
|
||||
{
|
||||
date: '2021-01-01',
|
||||
grossPerformance: new Big('349.35'),
|
||||
netPerformance: new Big('349.35'),
|
||||
investment: new Big('652.55'),
|
||||
value: new Big('1001.9') // 5 * (144.38 + days=700 * 0.08)
|
||||
value: new Big('1001.9') // 5 * 200.38
|
||||
},
|
||||
{
|
||||
date: '2021-02-01',
|
||||
grossPerformance: new Big('358.85'),
|
||||
netPerformance: new Big('358.85'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3042.9') // 15 * (144.38 + days=731 * 0.08)
|
||||
value: new Big('3042.9') // 15 * 202.86
|
||||
},
|
||||
{
|
||||
date: '2021-03-01',
|
||||
grossPerformance: new Big('392.45'),
|
||||
netPerformance: new Big('392.45'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3076.5') // 15 * (144.38 + days=759 * 0.08)
|
||||
value: new Big('3076.5') // 15 * 205.1
|
||||
},
|
||||
{
|
||||
date: '2021-04-01',
|
||||
grossPerformance: new Big('429.65'),
|
||||
netPerformance: new Big('429.65'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3113.7') // 15 * (144.38 + days=790 * 0.08)
|
||||
value: new Big('3113.7') // 15 * 207.58
|
||||
},
|
||||
{
|
||||
date: '2021-05-01',
|
||||
grossPerformance: new Big('465.65'),
|
||||
netPerformance: new Big('465.65'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3149.7') // 15 * (144.38 + days=820 * 0.08)
|
||||
value: new Big('3149.7') // 15 * 209.98
|
||||
},
|
||||
{
|
||||
date: '2021-06-01',
|
||||
grossPerformance: new Big('502.85'),
|
||||
netPerformance: new Big('502.85'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3186.9') // 15 * (144.38 + days=851 * 0.08)
|
||||
value: new Big('3186.9') // 15 * 212.46
|
||||
},
|
||||
{
|
||||
date: '2021-06-02',
|
||||
grossPerformance: new Big('504.05'),
|
||||
netPerformance: new Big('504.05'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3188.1') // 15 * (144.38 + days=852 * 0.08) / +1.2
|
||||
value: new Big('3188.1') // 15 * 212.54
|
||||
},
|
||||
{
|
||||
date: '2021-06-03',
|
||||
grossPerformance: new Big('505.25'),
|
||||
netPerformance: new Big('505.25'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3189.3') // +1.2
|
||||
value: new Big('3189.3') // 15 * 212.62
|
||||
},
|
||||
{
|
||||
date: '2021-06-04',
|
||||
grossPerformance: new Big('506.45'),
|
||||
netPerformance: new Big('506.45'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3190.5') // +1.2
|
||||
value: new Big('3190.5') // 15 * 212.7
|
||||
},
|
||||
{
|
||||
date: '2021-06-05',
|
||||
grossPerformance: new Big('507.65'),
|
||||
netPerformance: new Big('507.65'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3191.7') // +1.2
|
||||
value: new Big('3191.7') // 15 * 212.78
|
||||
},
|
||||
{
|
||||
date: '2021-06-06',
|
||||
grossPerformance: new Big('508.85'),
|
||||
netPerformance: new Big('508.85'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3192.9') // +1.2
|
||||
value: new Big('3192.9') // 15 * 212.86
|
||||
},
|
||||
{
|
||||
date: '2021-06-07',
|
||||
grossPerformance: new Big('510.05'),
|
||||
netPerformance: new Big('510.05'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3194.1') // +1.2
|
||||
value: new Big('3194.1') // 15 * 212.94
|
||||
},
|
||||
{
|
||||
date: '2021-06-08',
|
||||
grossPerformance: new Big('511.25'),
|
||||
netPerformance: new Big('511.25'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3195.3') // +1.2
|
||||
value: new Big('3195.3') // 15 * 213.02
|
||||
},
|
||||
{
|
||||
date: '2021-06-09',
|
||||
grossPerformance: new Big('512.45'),
|
||||
netPerformance: new Big('512.45'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3196.5') // +1.2
|
||||
value: new Big('3196.5') // 15 * 213.1
|
||||
},
|
||||
{
|
||||
date: '2021-06-10',
|
||||
grossPerformance: new Big('513.65'),
|
||||
netPerformance: new Big('513.65'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3197.7') // +1.2
|
||||
value: new Big('3197.7') // 15 * 213.18
|
||||
},
|
||||
{
|
||||
date: '2021-06-11',
|
||||
grossPerformance: new Big('514.85'),
|
||||
netPerformance: new Big('514.85'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3198.9') // +1.2
|
||||
value: new Big('3198.9') // 15 * 213.26
|
||||
},
|
||||
{
|
||||
date: '2021-06-12',
|
||||
grossPerformance: new Big('516.05'),
|
||||
netPerformance: new Big('516.05'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3200.1') // +1.2
|
||||
value: new Big('3200.1') // 15 * 213.34
|
||||
},
|
||||
{
|
||||
date: '2021-06-13',
|
||||
grossPerformance: new Big('517.25'),
|
||||
netPerformance: new Big('517.25'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3201.3') // +1.2
|
||||
value: new Big('3201.3') // 15 * 213.42
|
||||
},
|
||||
{
|
||||
date: '2021-06-14',
|
||||
grossPerformance: new Big('518.45'),
|
||||
netPerformance: new Big('518.45'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3202.5') // +1.2
|
||||
value: new Big('3202.5') // 15 * 213.5
|
||||
},
|
||||
{
|
||||
date: '2021-06-15',
|
||||
grossPerformance: new Big('519.65'),
|
||||
netPerformance: new Big('519.65'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3203.7') // +1.2
|
||||
value: new Big('3203.7') // 15 * 213.58
|
||||
},
|
||||
{
|
||||
date: '2021-06-16',
|
||||
grossPerformance: new Big('520.85'),
|
||||
netPerformance: new Big('520.85'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3204.9') // +1.2
|
||||
value: new Big('3204.9') // 15 * 213.66
|
||||
},
|
||||
{
|
||||
date: '2021-06-17',
|
||||
grossPerformance: new Big('522.05'),
|
||||
netPerformance: new Big('522.05'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3206.1') // +1.2
|
||||
value: new Big('3206.1') // 15 * 213.74
|
||||
},
|
||||
{
|
||||
date: '2021-06-18',
|
||||
grossPerformance: new Big('523.25'),
|
||||
netPerformance: new Big('523.25'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3207.3') // +1.2
|
||||
value: new Big('3207.3') // 15 * 213.82
|
||||
},
|
||||
{
|
||||
date: '2021-06-19',
|
||||
grossPerformance: new Big('524.45'),
|
||||
netPerformance: new Big('524.45'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3208.5') // +1.2
|
||||
value: new Big('3208.5') // 15 * 213.9
|
||||
},
|
||||
{
|
||||
date: '2021-06-20',
|
||||
grossPerformance: new Big('525.65'),
|
||||
netPerformance: new Big('525.65'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3209.7') // +1.2
|
||||
value: new Big('3209.7') // 15 * 213.98
|
||||
},
|
||||
{
|
||||
date: '2021-06-21',
|
||||
grossPerformance: new Big('526.85'),
|
||||
netPerformance: new Big('526.85'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3210.9') // +1.2
|
||||
value: new Big('3210.9') // 15 * 214.06
|
||||
},
|
||||
{
|
||||
date: '2021-06-22',
|
||||
grossPerformance: new Big('528.05'),
|
||||
netPerformance: new Big('528.05'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3212.1') // +1.2
|
||||
value: new Big('3212.1') // 15 * 214.14
|
||||
},
|
||||
{
|
||||
date: '2021-06-23',
|
||||
grossPerformance: new Big('529.25'),
|
||||
netPerformance: new Big('529.25'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3213.3') // +1.2
|
||||
value: new Big('3213.3') // 15 * 214.22
|
||||
},
|
||||
{
|
||||
date: '2021-06-24',
|
||||
grossPerformance: new Big('530.45'),
|
||||
netPerformance: new Big('530.45'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3214.5') // +1.2
|
||||
value: new Big('3214.5') // 15 * 214.3
|
||||
},
|
||||
{
|
||||
date: '2021-06-25',
|
||||
grossPerformance: new Big('531.65'),
|
||||
netPerformance: new Big('531.65'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3215.7') // +1.2
|
||||
value: new Big('3215.7') // 15 * 214.38
|
||||
},
|
||||
{
|
||||
date: '2021-06-26',
|
||||
grossPerformance: new Big('532.85'),
|
||||
netPerformance: new Big('532.85'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3216.9') // +1.2
|
||||
value: new Big('3216.9') // 15 * 214.46
|
||||
},
|
||||
{
|
||||
date: '2021-06-27',
|
||||
grossPerformance: new Big('534.05'),
|
||||
netPerformance: new Big('534.05'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3218.1') // +1.2
|
||||
value: new Big('3218.1') // 15 * 214.54
|
||||
},
|
||||
{
|
||||
date: '2021-06-28',
|
||||
grossPerformance: new Big('535.25'),
|
||||
netPerformance: new Big('535.25'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3219.3') // +1.2
|
||||
value: new Big('3219.3') // 15 * 214.62
|
||||
},
|
||||
{
|
||||
date: '2021-06-29',
|
||||
grossPerformance: new Big('536.45'),
|
||||
netPerformance: new Big('536.45'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3220.5') // +1.2
|
||||
value: new Big('3220.5') // 15 * 214.7
|
||||
},
|
||||
{
|
||||
date: '2021-06-30',
|
||||
grossPerformance: new Big('537.65'),
|
||||
netPerformance: new Big('537.65'),
|
||||
investment: new Big('2684.05'),
|
||||
value: new Big('3221.7') // +1.2
|
||||
value: new Big('3221.7') // 15 * 214.78
|
||||
}
|
||||
])
|
||||
);
|
||||
@ -2442,7 +2630,7 @@ describe('PortfolioCalculator', () => {
|
||||
grossPerformance: new Big('267.2'),
|
||||
netPerformance: new Big('267.2'),
|
||||
investment: new Big('11553.75'),
|
||||
value: new Big('11820.95') // 10 * (144.38 + days=334 * 0.08) + 5 * 2021.99
|
||||
value: new Big('11820.95') // 10 * 171.1 + 5 * 2021.99
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
@ -10,12 +10,12 @@ import { baseCurrency } from '@ghostfolio/common/config';
|
||||
import {
|
||||
PortfolioChart,
|
||||
PortfolioDetails,
|
||||
PortfolioInvestments,
|
||||
PortfolioPerformance,
|
||||
PortfolioPublicDetails,
|
||||
PortfolioReport,
|
||||
PortfolioSummary
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import {
|
||||
Controller,
|
||||
@ -48,42 +48,6 @@ export class PortfolioController {
|
||||
private readonly userService: UserService
|
||||
) {}
|
||||
|
||||
@Get('investments')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async findAll(
|
||||
@Headers('impersonation-id') impersonationId: string,
|
||||
@Res() res: Response
|
||||
): Promise<InvestmentItem[]> {
|
||||
if (
|
||||
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
|
||||
this.request.user.subscription.type === 'Basic'
|
||||
) {
|
||||
res.status(StatusCodes.FORBIDDEN);
|
||||
return <any>res.json([]);
|
||||
}
|
||||
|
||||
let investments = await this.portfolioService.getInvestments(
|
||||
impersonationId
|
||||
);
|
||||
|
||||
if (
|
||||
impersonationId ||
|
||||
this.userService.isRestrictedView(this.request.user)
|
||||
) {
|
||||
const maxInvestment = investments.reduce(
|
||||
(investment, item) => Math.max(investment, item.investment),
|
||||
1
|
||||
);
|
||||
|
||||
investments = investments.map((item) => ({
|
||||
date: item.date,
|
||||
investment: item.investment / maxInvestment
|
||||
}));
|
||||
}
|
||||
|
||||
return <any>res.json(investments);
|
||||
}
|
||||
|
||||
@Get('chart')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async getChart(
|
||||
@ -200,6 +164,42 @@ export class PortfolioController {
|
||||
return <any>res.json({ accounts, hasError, holdings });
|
||||
}
|
||||
|
||||
@Get('investments')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async getInvestments(
|
||||
@Headers('impersonation-id') impersonationId: string,
|
||||
@Res() res: Response
|
||||
): Promise<PortfolioInvestments> {
|
||||
if (
|
||||
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
|
||||
this.request.user.subscription.type === 'Basic'
|
||||
) {
|
||||
res.status(StatusCodes.FORBIDDEN);
|
||||
return <any>res.json({});
|
||||
}
|
||||
|
||||
let investments = await this.portfolioService.getInvestments(
|
||||
impersonationId
|
||||
);
|
||||
|
||||
if (
|
||||
impersonationId ||
|
||||
this.userService.isRestrictedView(this.request.user)
|
||||
) {
|
||||
const maxInvestment = investments.reduce(
|
||||
(investment, item) => Math.max(investment, item.investment),
|
||||
1
|
||||
);
|
||||
|
||||
investments = investments.map((item) => ({
|
||||
date: item.date,
|
||||
investment: item.investment / maxInvestment
|
||||
}));
|
||||
}
|
||||
|
||||
return <any>res.json({ firstOrderDate: investments[0]?.date, investments });
|
||||
}
|
||||
|
||||
@Get('performance')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async getPerformance(
|
||||
|
@ -55,7 +55,7 @@ import {
|
||||
subDays,
|
||||
subYears
|
||||
} from 'date-fns';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { isEmpty, sortBy } from 'lodash';
|
||||
|
||||
import {
|
||||
HistoricalDataContainer,
|
||||
@ -150,12 +150,33 @@ export class PortfolioService {
|
||||
return [];
|
||||
}
|
||||
|
||||
return portfolioCalculator.getInvestments().map((item) => {
|
||||
const investments = portfolioCalculator.getInvestments().map((item) => {
|
||||
return {
|
||||
date: item.date,
|
||||
investment: item.investment.toNumber()
|
||||
};
|
||||
});
|
||||
|
||||
// Add investment of today
|
||||
const investmentOfToday = investments.filter((investment) => {
|
||||
return investment.date === format(new Date(), DATE_FORMAT);
|
||||
});
|
||||
|
||||
if (investmentOfToday.length <= 0) {
|
||||
const pastInvestments = investments.filter((investment) => {
|
||||
return isBefore(parseDate(investment.date), new Date());
|
||||
});
|
||||
const lastInvestment = pastInvestments[pastInvestments.length - 1];
|
||||
|
||||
investments.push({
|
||||
date: format(new Date(), DATE_FORMAT),
|
||||
investment: lastInvestment?.investment ?? 0
|
||||
});
|
||||
}
|
||||
|
||||
return sortBy(investments, (investment) => {
|
||||
return investment.date;
|
||||
});
|
||||
}
|
||||
|
||||
public async getChart(
|
||||
@ -447,17 +468,17 @@ export class PortfolioService {
|
||||
// Convert investment, gross and net performance to currency of user
|
||||
const userCurrency = this.request.user.Settings.currency;
|
||||
const investment = this.exchangeRateDataService.toCurrency(
|
||||
position.investment.toNumber(),
|
||||
position.investment?.toNumber(),
|
||||
currency,
|
||||
userCurrency
|
||||
);
|
||||
const grossPerformance = this.exchangeRateDataService.toCurrency(
|
||||
position.grossPerformance.toNumber(),
|
||||
position.grossPerformance?.toNumber(),
|
||||
currency,
|
||||
userCurrency
|
||||
);
|
||||
const netPerformance = this.exchangeRateDataService.toCurrency(
|
||||
position.netPerformance.toNumber(),
|
||||
position.netPerformance?.toNumber(),
|
||||
currency,
|
||||
userCurrency
|
||||
);
|
||||
@ -662,7 +683,9 @@ export class PortfolioService {
|
||||
grossPerformancePercentage:
|
||||
position.grossPerformancePercentage?.toNumber() ?? null,
|
||||
investment: new Big(position.investment).toNumber(),
|
||||
marketState: dataProviderResponses[position.symbol].marketState,
|
||||
marketState:
|
||||
dataProviderResponses[position.symbol]?.marketState ??
|
||||
MarketState.delayed,
|
||||
name: symbolProfileMap[position.symbol].name,
|
||||
netPerformance: position.netPerformance?.toNumber() ?? null,
|
||||
netPerformancePercentage:
|
||||
|
@ -13,7 +13,7 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { DataSource, MarketData } from '@prisma/client';
|
||||
import { DataSource } from '@prisma/client';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
import { isDate, isEmpty } from 'lodash';
|
||||
|
||||
@ -37,8 +37,7 @@ export class SymbolController {
|
||||
@Query() { query = '' }
|
||||
): Promise<{ items: LookupItem[] }> {
|
||||
try {
|
||||
const encodedQuery = encodeURIComponent(query.toLowerCase());
|
||||
return this.symbolService.lookup(encodedQuery);
|
||||
return this.symbolService.lookup(query.toLowerCase());
|
||||
} catch {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR),
|
||||
|
@ -93,32 +93,6 @@ export class SymbolService {
|
||||
try {
|
||||
const { items } = await this.dataProviderService.search(aQuery);
|
||||
results.items = items;
|
||||
|
||||
// Add custom symbols
|
||||
const ghostfolioSymbolProfiles =
|
||||
await this.prismaService.symbolProfile.findMany({
|
||||
select: {
|
||||
currency: true,
|
||||
dataSource: true,
|
||||
name: true,
|
||||
symbol: true
|
||||
},
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
dataSource: DataSource.GHOSTFOLIO,
|
||||
name: {
|
||||
startsWith: aQuery
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
for (const ghostfolioSymbolProfile of ghostfolioSymbolProfiles) {
|
||||
results.items.push(ghostfolioSymbolProfile);
|
||||
}
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
Logger.error(error);
|
||||
|
@ -13,6 +13,7 @@ export class ConfigurationService {
|
||||
ACCESS_TOKEN_SALT: str(),
|
||||
ALPHA_VANTAGE_API_KEY: str({ default: '' }),
|
||||
CACHE_TTL: num({ default: 1 }),
|
||||
DATA_SOURCE_PRIMARY: str({ default: DataSource.YAHOO }),
|
||||
DATA_SOURCES: json({ default: JSON.stringify([DataSource.YAHOO]) }),
|
||||
ENABLE_FEATURE_BLOG: bool({ default: false }),
|
||||
ENABLE_FEATURE_CUSTOM_SYMBOLS: bool({ default: false }),
|
||||
@ -25,6 +26,9 @@ export class ConfigurationService {
|
||||
ENABLE_FEATURE_SYSTEM_MESSAGE: bool({ default: false }),
|
||||
GOOGLE_CLIENT_ID: str({ default: 'dummyClientId' }),
|
||||
GOOGLE_SECRET: str({ default: 'dummySecret' }),
|
||||
GOOGLE_SHEETS_ACCOUNT: str({ default: '' }),
|
||||
GOOGLE_SHEETS_ID: str({ default: '' }),
|
||||
GOOGLE_SHEETS_PRIVATE_KEY: str({ default: '' }),
|
||||
JWT_SECRET_KEY: str({}),
|
||||
MAX_ITEM_IN_CACHE: num({ default: 9999 }),
|
||||
MAX_ORDERS_TO_IMPORT: num({ default: Number.MAX_SAFE_INTEGER }),
|
||||
|
@ -88,13 +88,13 @@ export class AlphaVantageService implements DataProviderInterface {
|
||||
return DataSource.ALPHA_VANTAGE;
|
||||
}
|
||||
|
||||
public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
|
||||
const result = await this.alphaVantage.data.search(aSymbol);
|
||||
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
||||
const result = await this.alphaVantage.data.search(aQuery);
|
||||
|
||||
return {
|
||||
items: result?.bestMatches?.map((bestMatch) => {
|
||||
return {
|
||||
dataSource: DataSource.ALPHA_VANTAGE,
|
||||
dataSource: this.getName(),
|
||||
name: bestMatch['2. name'],
|
||||
symbol: bestMatch['1. symbol']
|
||||
};
|
||||
|
@ -7,6 +7,9 @@ const getJSON = bent('json');
|
||||
export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
||||
private static baseUrl = 'https://data.trackinsight.com/holdings';
|
||||
private static countries = require('countries-list/dist/countries.json');
|
||||
private static countriesMapping = {
|
||||
'Russian Federation': 'Russia'
|
||||
};
|
||||
private static sectorsMapping = {
|
||||
'Consumer Discretionary': 'Consumer Cyclical',
|
||||
'Consumer Defensive': 'Consumer Staples',
|
||||
@ -45,7 +48,11 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
||||
for (const [key, country] of Object.entries<any>(
|
||||
TrackinsightDataEnhancerService.countries
|
||||
)) {
|
||||
if (country.name === name) {
|
||||
if (
|
||||
country.name === name ||
|
||||
country.name ===
|
||||
TrackinsightDataEnhancerService.countriesMapping[name]
|
||||
) {
|
||||
countryCode = key;
|
||||
break;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||
import { CryptocurrencyModule } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.module';
|
||||
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
||||
import { GoogleSheetsService } from '@ghostfolio/api/services/data-provider/google-sheets/google-sheets.service';
|
||||
import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
|
||||
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
|
||||
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
||||
@ -21,12 +22,14 @@ import { DataProviderService } from './data-provider.service';
|
||||
AlphaVantageService,
|
||||
DataProviderService,
|
||||
GhostfolioScraperApiService,
|
||||
GoogleSheetsService,
|
||||
RakutenRapidApiService,
|
||||
YahooFinanceService,
|
||||
{
|
||||
inject: [
|
||||
AlphaVantageService,
|
||||
GhostfolioScraperApiService,
|
||||
GoogleSheetsService,
|
||||
RakutenRapidApiService,
|
||||
YahooFinanceService
|
||||
],
|
||||
@ -34,11 +37,13 @@ import { DataProviderService } from './data-provider.service';
|
||||
useFactory: (
|
||||
alphaVantageService,
|
||||
ghostfolioScraperApiService,
|
||||
googleSheetsService,
|
||||
rakutenRapidApiService,
|
||||
yahooFinanceService
|
||||
) => [
|
||||
alphaVantageService,
|
||||
ghostfolioScraperApiService,
|
||||
googleSheetsService,
|
||||
rakutenRapidApiService,
|
||||
yahooFinanceService
|
||||
]
|
||||
|
@ -149,13 +149,13 @@ export class DataProviderService {
|
||||
return result;
|
||||
}
|
||||
|
||||
public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
|
||||
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
||||
const promises: Promise<{ items: LookupItem[] }>[] = [];
|
||||
let lookupItems: LookupItem[] = [];
|
||||
|
||||
for (const dataSource of this.configurationService.get('DATA_SOURCES')) {
|
||||
promises.push(
|
||||
this.getDataProvider(DataSource[dataSource]).search(aSymbol)
|
||||
this.getDataProvider(DataSource[dataSource]).search(aQuery)
|
||||
);
|
||||
}
|
||||
|
||||
@ -176,7 +176,7 @@ export class DataProviderService {
|
||||
}
|
||||
|
||||
public getPrimaryDataSource(): DataSource {
|
||||
return DataSource[this.configurationService.get('DATA_SOURCES')[0]];
|
||||
return DataSource[this.configurationService.get('DATA_SOURCE_PRIMARY')];
|
||||
}
|
||||
|
||||
private getDataProvider(providerName: DataSource) {
|
||||
|
@ -1,4 +1,10 @@
|
||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||
import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
|
||||
import {
|
||||
IDataProviderHistoricalResponse,
|
||||
IDataProviderResponse,
|
||||
MarketState
|
||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
||||
import {
|
||||
@ -13,13 +19,6 @@ import * as bent from 'bent';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
import {
|
||||
IDataProviderHistoricalResponse,
|
||||
IDataProviderResponse,
|
||||
MarketState
|
||||
} from '../../interfaces/interfaces';
|
||||
import { DataProviderInterface } from '../interfaces/data-provider.interface';
|
||||
|
||||
@Injectable()
|
||||
export class GhostfolioScraperApiService implements DataProviderInterface {
|
||||
private static NUMERIC_REGEXP = /[-]{0,1}[\d]*[.,]{0,1}[\d]+/g;
|
||||
@ -59,7 +58,7 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
||||
[symbol]: {
|
||||
marketPrice,
|
||||
currency: symbolProfile?.currency,
|
||||
dataSource: DataSource.GHOSTFOLIO,
|
||||
dataSource: this.getName(),
|
||||
marketState: MarketState.delayed
|
||||
}
|
||||
};
|
||||
@ -116,8 +115,35 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
||||
return DataSource.GHOSTFOLIO;
|
||||
}
|
||||
|
||||
public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
|
||||
return { items: [] };
|
||||
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
||||
const items = await this.prismaService.symbolProfile.findMany({
|
||||
select: {
|
||||
currency: true,
|
||||
dataSource: true,
|
||||
name: true,
|
||||
symbol: true
|
||||
},
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
dataSource: this.getName(),
|
||||
name: {
|
||||
mode: 'insensitive',
|
||||
startsWith: aQuery
|
||||
}
|
||||
},
|
||||
{
|
||||
dataSource: this.getName(),
|
||||
symbol: {
|
||||
mode: 'insensitive',
|
||||
startsWith: aQuery
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
return { items };
|
||||
}
|
||||
|
||||
private extractNumberFromString(aString: string): number {
|
||||
|
@ -0,0 +1,172 @@
|
||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
|
||||
import {
|
||||
IDataProviderHistoricalResponse,
|
||||
IDataProviderResponse,
|
||||
MarketState
|
||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||
import { Granularity } from '@ghostfolio/common/types';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { DataSource } from '@prisma/client';
|
||||
import { format } from 'date-fns';
|
||||
import { GoogleSpreadsheet } from 'google-spreadsheet';
|
||||
|
||||
@Injectable()
|
||||
export class GoogleSheetsService implements DataProviderInterface {
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService,
|
||||
private readonly prismaService: PrismaService,
|
||||
private readonly symbolProfileService: SymbolProfileService
|
||||
) {}
|
||||
|
||||
public canHandle(symbol: string) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public async get(
|
||||
aSymbols: string[]
|
||||
): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
||||
if (aSymbols.length <= 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
const [symbol] = aSymbols;
|
||||
const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles(
|
||||
[symbol]
|
||||
);
|
||||
|
||||
const sheet = await this.getSheet({
|
||||
sheetId: this.configurationService.get('GOOGLE_SHEETS_ID'),
|
||||
symbol
|
||||
});
|
||||
const marketPrice = parseFloat(
|
||||
(await sheet.getCellByA1('B1').value) as string
|
||||
);
|
||||
|
||||
return {
|
||||
[symbol]: {
|
||||
marketPrice,
|
||||
currency: symbolProfile?.currency,
|
||||
dataSource: this.getName(),
|
||||
marketState: MarketState.delayed
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
Logger.error(error);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
public async getHistorical(
|
||||
aSymbols: string[],
|
||||
aGranularity: Granularity = 'day',
|
||||
from: Date,
|
||||
to: Date
|
||||
): Promise<{
|
||||
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
||||
}> {
|
||||
if (aSymbols.length <= 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
const [symbol] = aSymbols;
|
||||
|
||||
const sheet = await this.getSheet({
|
||||
symbol,
|
||||
sheetId: this.configurationService.get('GOOGLE_SHEETS_ID')
|
||||
});
|
||||
|
||||
const rows = await sheet.getRows();
|
||||
|
||||
const historicalData: {
|
||||
[date: string]: IDataProviderHistoricalResponse;
|
||||
} = {};
|
||||
|
||||
rows
|
||||
.filter((row, index) => {
|
||||
return index >= 1;
|
||||
})
|
||||
.forEach((row) => {
|
||||
const date = new Date(row._rawData[0]);
|
||||
const close = parseFloat(row._rawData[1]);
|
||||
|
||||
historicalData[format(date, DATE_FORMAT)] = { marketPrice: close };
|
||||
});
|
||||
|
||||
return {
|
||||
[symbol]: historicalData
|
||||
};
|
||||
} catch (error) {
|
||||
Logger.error(error);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
public getName(): DataSource {
|
||||
return DataSource.GOOGLE_SHEETS;
|
||||
}
|
||||
|
||||
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
||||
const items = await this.prismaService.symbolProfile.findMany({
|
||||
select: {
|
||||
currency: true,
|
||||
dataSource: true,
|
||||
name: true,
|
||||
symbol: true
|
||||
},
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
dataSource: this.getName(),
|
||||
name: {
|
||||
mode: 'insensitive',
|
||||
startsWith: aQuery
|
||||
}
|
||||
},
|
||||
{
|
||||
dataSource: this.getName(),
|
||||
symbol: {
|
||||
mode: 'insensitive',
|
||||
startsWith: aQuery
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
return { items };
|
||||
}
|
||||
|
||||
private async getSheet({
|
||||
sheetId,
|
||||
symbol
|
||||
}: {
|
||||
sheetId: string;
|
||||
symbol: string;
|
||||
}) {
|
||||
const doc = new GoogleSpreadsheet(sheetId);
|
||||
|
||||
await doc.useServiceAccountAuth({
|
||||
client_email: this.configurationService.get('GOOGLE_SHEETS_ACCOUNT'),
|
||||
private_key: this.configurationService
|
||||
.get('GOOGLE_SHEETS_PRIVATE_KEY')
|
||||
.replace(/\\n/g, '\n')
|
||||
});
|
||||
|
||||
await doc.loadInfo();
|
||||
|
||||
const sheet = doc.sheetsByTitle[symbol];
|
||||
|
||||
await sheet.loadCells();
|
||||
|
||||
return sheet;
|
||||
}
|
||||
}
|
@ -1,11 +1,10 @@
|
||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||
import { Granularity } from '@ghostfolio/common/types';
|
||||
import { DataSource } from '@prisma/client';
|
||||
|
||||
import {
|
||||
IDataProviderHistoricalResponse,
|
||||
IDataProviderResponse
|
||||
} from '../../interfaces/interfaces';
|
||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { Granularity } from '@ghostfolio/common/types';
|
||||
import { DataSource } from '@prisma/client';
|
||||
|
||||
export interface DataProviderInterface {
|
||||
canHandle(symbol: string): boolean;
|
||||
@ -23,5 +22,5 @@ export interface DataProviderInterface {
|
||||
|
||||
getName(): DataSource;
|
||||
|
||||
search(aSymbol: string): Promise<{ items: LookupItem[] }>;
|
||||
search(aQuery: string): Promise<{ items: LookupItem[] }>;
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
||||
return {
|
||||
[ghostfolioFearAndGreedIndexSymbol]: {
|
||||
currency: undefined,
|
||||
dataSource: DataSource.RAKUTEN,
|
||||
dataSource: this.getName(),
|
||||
marketPrice: fgi.now.value,
|
||||
marketState: MarketState.open,
|
||||
name: RakutenRapidApiService.FEAR_AND_GREED_INDEX_NAME
|
||||
@ -85,7 +85,7 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
||||
await this.prismaService.marketData.create({
|
||||
data: {
|
||||
symbol,
|
||||
dataSource: DataSource.RAKUTEN,
|
||||
dataSource: this.getName(),
|
||||
date: subWeeks(getToday(), 1),
|
||||
marketPrice: fgi.oneWeekAgo.value
|
||||
}
|
||||
@ -94,7 +94,7 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
||||
await this.prismaService.marketData.create({
|
||||
data: {
|
||||
symbol,
|
||||
dataSource: DataSource.RAKUTEN,
|
||||
dataSource: this.getName(),
|
||||
date: subMonths(getToday(), 1),
|
||||
marketPrice: fgi.oneMonthAgo.value
|
||||
}
|
||||
@ -103,7 +103,7 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
||||
await this.prismaService.marketData.create({
|
||||
data: {
|
||||
symbol,
|
||||
dataSource: DataSource.RAKUTEN,
|
||||
dataSource: this.getName(),
|
||||
date: subYears(getToday(), 1),
|
||||
marketPrice: fgi.oneYearAgo.value
|
||||
}
|
||||
@ -129,7 +129,7 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
||||
return DataSource.RAKUTEN;
|
||||
}
|
||||
|
||||
public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
|
||||
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
||||
return { items: [] };
|
||||
}
|
||||
|
||||
|
@ -103,7 +103,7 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
assetClass,
|
||||
assetSubClass,
|
||||
currency: value.price?.currency,
|
||||
dataSource: DataSource.YAHOO,
|
||||
dataSource: this.getName(),
|
||||
exchange: this.parseExchange(value.price?.exchangeName),
|
||||
marketState:
|
||||
value.price?.marketState === 'REGULAR' ||
|
||||
@ -221,12 +221,14 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
return DataSource.YAHOO;
|
||||
}
|
||||
|
||||
public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
|
||||
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
||||
const items: LookupItem[] = [];
|
||||
|
||||
try {
|
||||
const get = bent(
|
||||
`${this.yahooFinanceHostname}/v1/finance/search?q=${aSymbol}&lang=en-US®ion=US"esCount=8&newsCount=0&enableFuzzyQuery=false"esQueryId=tss_match_phrase_query&multiQuoteQueryId=multi_quote_single_token_query&newsQueryId=news_cie_vespa&enableCb=true&enableNavLinks=false&enableEnhancedTrivialQuery=true`,
|
||||
`${this.yahooFinanceHostname}/v1/finance/search?q=${encodeURIComponent(
|
||||
aQuery
|
||||
)}&lang=en-US®ion=US"esCount=8&newsCount=0&enableFuzzyQuery=false"esQueryId=tss_match_phrase_query&multiQuoteQueryId=multi_quote_single_token_query&newsQueryId=news_cie_vespa&enableCb=true&enableNavLinks=false&enableEnhancedTrivialQuery=true`,
|
||||
'GET',
|
||||
'json',
|
||||
200
|
||||
@ -268,7 +270,7 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
items.push({
|
||||
symbol,
|
||||
currency: value.currency,
|
||||
dataSource: DataSource.YAHOO,
|
||||
dataSource: this.getName(),
|
||||
name: value.name
|
||||
});
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ export interface Environment extends CleanedEnvAccessors {
|
||||
ACCESS_TOKEN_SALT: string;
|
||||
ALPHA_VANTAGE_API_KEY: string;
|
||||
CACHE_TTL: number;
|
||||
DATA_SOURCE_PRIMARY: string;
|
||||
DATA_SOURCES: string | string[]; // string is not correct, error in envalid?
|
||||
ENABLE_FEATURE_BLOG: boolean;
|
||||
ENABLE_FEATURE_CUSTOM_SYMBOLS: boolean;
|
||||
@ -16,6 +17,9 @@ export interface Environment extends CleanedEnvAccessors {
|
||||
ENABLE_FEATURE_SYSTEM_MESSAGE: boolean;
|
||||
GOOGLE_CLIENT_ID: string;
|
||||
GOOGLE_SECRET: string;
|
||||
GOOGLE_SHEETS_ACCOUNT: string;
|
||||
GOOGLE_SHEETS_ID: string;
|
||||
GOOGLE_SHEETS_PRIVATE_KEY: string;
|
||||
JWT_SECRET_KEY: string;
|
||||
MAX_ITEM_IN_CACHE: number;
|
||||
MAX_ORDERS_TO_IMPORT: number;
|
||||
|
@ -9,6 +9,13 @@ const routes: Routes = [
|
||||
loadChildren: () =>
|
||||
import('./pages/about/about-page.module').then((m) => m.AboutPageModule)
|
||||
},
|
||||
{
|
||||
path: 'about/changelog',
|
||||
loadChildren: () =>
|
||||
import('./pages/about/changelog/changelog-page.module').then(
|
||||
(m) => m.ChangelogPageModule
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'account',
|
||||
loadChildren: () =>
|
||||
@ -33,6 +40,11 @@ const routes: Routes = [
|
||||
loadChildren: () =>
|
||||
import('./pages/auth/auth-page.module').then((m) => m.AuthPageModule)
|
||||
},
|
||||
{
|
||||
path: 'blog',
|
||||
loadChildren: () =>
|
||||
import('./pages/blog/blog-page.module').then((m) => m.BlogPageModule)
|
||||
},
|
||||
{
|
||||
path: 'de/blog/2021/07/hallo-ghostfolio',
|
||||
loadChildren: () =>
|
||||
@ -47,6 +59,13 @@ const routes: Routes = [
|
||||
'./pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.module'
|
||||
).then((m) => m.HelloGhostfolioPageModule)
|
||||
},
|
||||
{
|
||||
path: 'en/blog/2022/01/ghostfolio-first-months-in-open-source',
|
||||
loadChildren: () =>
|
||||
import(
|
||||
'./pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.module'
|
||||
).then((m) => m.FirstMonthsInOpenSourcePageModule)
|
||||
},
|
||||
{
|
||||
path: 'home',
|
||||
loadChildren: () =>
|
||||
@ -66,6 +85,13 @@ const routes: Routes = [
|
||||
(m) => m.PortfolioPageModule
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'portfolio/activities',
|
||||
loadChildren: () =>
|
||||
import('./pages/portfolio/transactions/transactions-page.module').then(
|
||||
(m) => m.TransactionsPageModule
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'portfolio/allocations',
|
||||
loadChildren: () =>
|
||||
@ -87,13 +113,6 @@ const routes: Routes = [
|
||||
(m) => m.ReportPageModule
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'portfolio/transactions',
|
||||
loadChildren: () =>
|
||||
import('./pages/portfolio/transactions/transactions-page.module').then(
|
||||
(m) => m.TransactionsPageModule
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'pricing',
|
||||
loadChildren: () =>
|
||||
|
@ -5,10 +5,10 @@ import { MatInputModule } from '@angular/material/input';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module';
|
||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
|
||||
import { GfSymbolIconModule } from '../symbol-icon/symbol-icon.module';
|
||||
import { AccountsTableComponent } from './accounts-table.component';
|
||||
|
||||
@NgModule({
|
||||
|
@ -43,6 +43,19 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
|
||||
this.fetchAdminMarketData();
|
||||
}
|
||||
|
||||
public onGatherProfileDataBySymbol({
|
||||
dataSource,
|
||||
symbol
|
||||
}: {
|
||||
dataSource: DataSource;
|
||||
symbol: string;
|
||||
}) {
|
||||
this.adminService
|
||||
.gatherProfileDataBySymbol({ dataSource, symbol })
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {});
|
||||
}
|
||||
|
||||
public onGatherSymbol({
|
||||
dataSource,
|
||||
symbol
|
||||
|
@ -38,6 +38,13 @@
|
||||
>
|
||||
Gather Data
|
||||
</button>
|
||||
<button
|
||||
i18n
|
||||
mat-menu-item
|
||||
(click)="onGatherProfileDataBySymbol({dataSource: item.dataSource, symbol: item.symbol})"
|
||||
>
|
||||
Gather Profile Data
|
||||
</button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -48,7 +48,7 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((params) => {
|
||||
if (params['positionDetailDialog'] && params['symbol']) {
|
||||
this.openDialog(params['symbol']);
|
||||
this.openPositionDialog({ symbol: params['symbol'] });
|
||||
}
|
||||
});
|
||||
|
||||
@ -91,24 +91,31 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
private openDialog(aSymbol: string): void {
|
||||
const dialogRef = this.dialog.open(PositionDetailDialog, {
|
||||
autoFocus: false,
|
||||
data: {
|
||||
baseCurrency: this.user?.settings?.baseCurrency,
|
||||
deviceType: this.deviceType,
|
||||
locale: this.user?.settings?.locale,
|
||||
symbol: aSymbol
|
||||
},
|
||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||
});
|
||||
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
private openPositionDialog({ symbol }: { symbol: string }) {
|
||||
this.userService
|
||||
.get()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
this.router.navigate(['.'], { relativeTo: this.route });
|
||||
.subscribe((user) => {
|
||||
this.user = user;
|
||||
|
||||
const dialogRef = this.dialog.open(PositionDetailDialog, {
|
||||
autoFocus: false,
|
||||
data: {
|
||||
symbol,
|
||||
baseCurrency: this.user?.settings?.baseCurrency,
|
||||
deviceType: this.deviceType,
|
||||
locale: this.user?.settings?.locale
|
||||
},
|
||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||
});
|
||||
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
this.router.navigate(['.'], { relativeTo: this.route });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,7 @@
|
||||
<gf-positions
|
||||
[baseCurrency]="user?.settings?.baseCurrency"
|
||||
[deviceType]="deviceType"
|
||||
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
||||
[locale]="user?.settings?.locale"
|
||||
[positions]="positions"
|
||||
[range]="dateRange"
|
||||
@ -25,8 +26,8 @@
|
||||
class="mt-3"
|
||||
i18n
|
||||
mat-button
|
||||
[routerLink]="['/portfolio', 'transactions']"
|
||||
>Manage Transactions...</a
|
||||
[routerLink]="['/portfolio', 'activities']"
|
||||
>Manage Activities...</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { defaultDateRangeOptions } from '@ghostfolio/common/config';
|
||||
import { PortfolioPerformance, User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { DateRange } from '@ghostfolio/common/types';
|
||||
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
@ -25,6 +26,7 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
||||
public deviceType: string;
|
||||
public hasError: boolean;
|
||||
public hasImpersonationId: boolean;
|
||||
public hasPermissionToCreateOrder: boolean;
|
||||
public historicalDataItems: LineChartItem[];
|
||||
public isAllTimeHigh: boolean;
|
||||
public isAllTimeLow: boolean;
|
||||
@ -51,6 +53,11 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
|
||||
this.hasPermissionToCreateOrder = hasPermission(
|
||||
this.user.permissions,
|
||||
permissions.createOrder
|
||||
);
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
|
@ -13,7 +13,7 @@
|
||||
[showYAxis]="false"
|
||||
></gf-line-chart>
|
||||
<div
|
||||
*ngIf="historicalDataItems?.length === 0"
|
||||
*ngIf="hasPermissionToCreateOrder&& historicalDataItems?.length === 0"
|
||||
class="align-items-center d-flex h-100 justify-content-center w-100"
|
||||
>
|
||||
<div class="d-flex justify-content-center">
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { primaryColorRgb } from '@ghostfolio/common/config';
|
||||
import { parseDate } from '@ghostfolio/common/helper';
|
||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||
import {
|
||||
Chart,
|
||||
@ -19,7 +20,7 @@ import {
|
||||
PointElement,
|
||||
TimeScale
|
||||
} from 'chart.js';
|
||||
import { addMonths, isAfter, parseISO, subMonths } from 'date-fns';
|
||||
import { addDays, isAfter, parseISO, subDays } from 'date-fns';
|
||||
|
||||
@Component({
|
||||
selector: 'gf-investment-chart',
|
||||
@ -27,8 +28,10 @@ import { addMonths, isAfter, parseISO, subMonths } from 'date-fns';
|
||||
templateUrl: './investment-chart.component.html',
|
||||
styleUrls: ['./investment-chart.component.scss']
|
||||
})
|
||||
export class InvestmentChartComponent implements OnChanges, OnDestroy, OnInit {
|
||||
export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
@Input() daysInMarket: number;
|
||||
@Input() investments: InvestmentItem[];
|
||||
@Input() isInPercent = false;
|
||||
|
||||
@ViewChild('chartCanvas') chartCanvas;
|
||||
|
||||
@ -45,8 +48,6 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy, OnInit {
|
||||
);
|
||||
}
|
||||
|
||||
public ngOnInit() {}
|
||||
|
||||
public ngOnChanges() {
|
||||
if (this.investments) {
|
||||
this.initialize();
|
||||
@ -61,19 +62,25 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy, OnInit {
|
||||
this.isLoading = true;
|
||||
|
||||
if (this.investments?.length > 0) {
|
||||
// Extend chart by three months (before)
|
||||
// Extend chart by 5% of days in market (before)
|
||||
const firstItem = this.investments[0];
|
||||
this.investments.unshift({
|
||||
...firstItem,
|
||||
date: subMonths(parseISO(firstItem.date), 3).toISOString(),
|
||||
date: subDays(
|
||||
parseISO(firstItem.date),
|
||||
this.daysInMarket * 0.05 || 90
|
||||
).toISOString(),
|
||||
investment: 0
|
||||
});
|
||||
|
||||
// Extend chart by three months (after)
|
||||
// Extend chart by 5% of days in market (after)
|
||||
const lastItem = this.investments[this.investments.length - 1];
|
||||
this.investments.push({
|
||||
...lastItem,
|
||||
date: addMonths(new Date(), 3).toISOString()
|
||||
date: addDays(
|
||||
parseDate(lastItem.date),
|
||||
this.daysInMarket * 0.05 || 90
|
||||
).toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
@ -136,12 +143,26 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy, OnInit {
|
||||
}
|
||||
},
|
||||
y: {
|
||||
display: false,
|
||||
display: !this.isInPercent,
|
||||
grid: {
|
||||
display: false
|
||||
},
|
||||
ticks: {
|
||||
display: false
|
||||
display: true,
|
||||
callback: (tickValue, index, ticks) => {
|
||||
if (index === 0 || index === ticks.length - 1) {
|
||||
// Only print last and first legend entry
|
||||
if (typeof tickValue === 'number') {
|
||||
return tickValue.toFixed(2);
|
||||
}
|
||||
|
||||
return tickValue;
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
mirror: true,
|
||||
z: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import { InvestmentChartComponent } from './investment-chart.component';
|
||||
@NgModule({
|
||||
declarations: [InvestmentChartComponent],
|
||||
exports: [InvestmentChartComponent],
|
||||
imports: [CommonModule, NgxSkeletonLoaderModule],
|
||||
providers: []
|
||||
imports: [CommonModule, NgxSkeletonLoaderModule]
|
||||
})
|
||||
export class GfInvestmentChartModule {}
|
||||
|
@ -3,12 +3,15 @@
|
||||
<div
|
||||
class="flex-grow-1 status text-muted text-right"
|
||||
[title]="
|
||||
hasError
|
||||
hasError && !isLoading
|
||||
? 'Sorry! Our data provider partner is experiencing the hiccups.'
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<ion-icon *ngIf="hasError" name="alert-circle-outline"></ion-icon>
|
||||
<ion-icon
|
||||
*ngIf="hasError && !isLoading"
|
||||
name="alert-circle-outline"
|
||||
></ion-icon>
|
||||
</div>
|
||||
<div *ngIf="isLoading" class="align-items-center d-flex">
|
||||
<ngx-skeleton-loader
|
||||
|
@ -12,7 +12,7 @@
|
||||
<div class="col-12 d-flex justify-content-center mb-3">
|
||||
<gf-value
|
||||
size="large"
|
||||
[currency]="data.baseCurrency"
|
||||
[currency]="currency"
|
||||
[locale]="data.locale"
|
||||
[value]="value"
|
||||
></gf-value>
|
||||
@ -125,19 +125,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<gf-transactions-table
|
||||
<gf-activities-table
|
||||
*ngIf="orders?.length > 0"
|
||||
[activities]="orders"
|
||||
[baseCurrency]="data.baseCurrency"
|
||||
[deviceType]="data.deviceType"
|
||||
[hasPermissionToCreateOrder]="false"
|
||||
[hasPermissionToCreateActivity]="false"
|
||||
[hasPermissionToFilter]="false"
|
||||
[hasPermissionToImportOrders]="false"
|
||||
[hasPermissionToImportActivities]="false"
|
||||
[hasPermissionToOpenDetails]="false"
|
||||
[locale]="data.locale"
|
||||
[showActions]="false"
|
||||
[showSymbolColumn]="false"
|
||||
[transactions]="orders"
|
||||
></gf-transactions-table>
|
||||
></gf-activities-table>
|
||||
</div>
|
||||
|
||||
<gf-dialog-footer
|
||||
|
@ -4,7 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
|
||||
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
|
||||
import { GfTransactionsTableModule } from '@ghostfolio/client/components/transactions-table/transactions-table.module';
|
||||
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
|
||||
import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
|
||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
@ -16,10 +16,10 @@ import { PositionDetailDialog } from './position-detail-dialog.component';
|
||||
exports: [],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfActivitiesTableModule,
|
||||
GfDialogFooterModule,
|
||||
GfDialogHeaderModule,
|
||||
GfLineChartModule,
|
||||
GfTransactionsTableModule,
|
||||
GfValueModule,
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
|
@ -123,7 +123,12 @@
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
|
||||
<div *ngIf="dataSource.data.length === 0 && !isLoading" class="p-3 text-center">
|
||||
<div
|
||||
*ngIf="
|
||||
dataSource.data.length === 0 && hasPermissionToCreateOrder && !isLoading
|
||||
"
|
||||
class="p-3 text-center"
|
||||
>
|
||||
<gf-no-transactions-info-indicator
|
||||
[hasBorder]="false"
|
||||
></gf-no-transactions-info-indicator>
|
||||
|
@ -9,16 +9,13 @@ import {
|
||||
Output,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatPaginator } from '@angular/material/paginator';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
|
||||
import { Router } from '@angular/router';
|
||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||
import { AssetClass, Order as OrderModel } from '@prisma/client';
|
||||
import { Subject, Subscription } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'gf-positions-table',
|
||||
@ -29,6 +26,7 @@ import { takeUntil } from 'rxjs/operators';
|
||||
export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
|
||||
@Input() baseCurrency: string;
|
||||
@Input() deviceType: string;
|
||||
@Input() hasPermissionToCreateOrder: boolean;
|
||||
@Input() locale: string;
|
||||
@Input() positions: PortfolioPosition[];
|
||||
|
||||
@ -48,21 +46,7 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor(
|
||||
private dialog: MatDialog,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router
|
||||
) {
|
||||
this.routeQueryParams = route.queryParams
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((params) => {
|
||||
if (params['positionDetailDialog'] && params['symbol']) {
|
||||
this.openPositionDialog({
|
||||
symbol: params['symbol']
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
public constructor(private router: Router) {}
|
||||
|
||||
public ngOnInit() {}
|
||||
|
||||
@ -105,27 +89,6 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
public openPositionDialog({ symbol }: { symbol: string }): void {
|
||||
const dialogRef = this.dialog.open(PositionDetailDialog, {
|
||||
autoFocus: false,
|
||||
data: {
|
||||
symbol,
|
||||
baseCurrency: this.baseCurrency,
|
||||
deviceType: this.deviceType,
|
||||
locale: this.locale
|
||||
},
|
||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||
});
|
||||
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
this.router.navigate(['.'], { relativeTo: this.route });
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
|
@ -23,7 +23,10 @@
|
||||
[range]="range"
|
||||
></gf-position>
|
||||
</ng-container>
|
||||
<div *ngIf="!hasPositions" class="p-3 text-center">
|
||||
<div
|
||||
*ngIf="hasPermissionToCreateOrder && !hasPositions"
|
||||
class="p-3 text-center"
|
||||
>
|
||||
<gf-no-transactions-info-indicator
|
||||
[hasBorder]="false"
|
||||
></gf-no-transactions-info-indicator>
|
||||
|
@ -17,6 +17,7 @@ import { Position } from '@ghostfolio/common/interfaces';
|
||||
export class PositionsComponent implements OnChanges, OnInit {
|
||||
@Input() baseCurrency: string;
|
||||
@Input() deviceType: string;
|
||||
@Input() hasPermissionToCreateOrder: boolean;
|
||||
@Input() locale: string;
|
||||
@Input() positions: Position[];
|
||||
@Input() range: string;
|
||||
|
@ -1,7 +1,10 @@
|
||||
<div class="container p-0">
|
||||
<div class="row no-gutters">
|
||||
<div class="col">
|
||||
<mat-card *ngIf="rules === null" class="my-2 text-center">
|
||||
<mat-card
|
||||
*ngIf="hasPermissionToCreateOrder && rules === null"
|
||||
class="my-2 text-center"
|
||||
>
|
||||
<gf-no-transactions-info-indicator
|
||||
[hasBorder]="false"
|
||||
></gf-no-transactions-info-indicator>
|
||||
|
@ -8,6 +8,7 @@ import { PortfolioReportRule } from '@ghostfolio/common/interfaces';
|
||||
styleUrls: ['./rules.component.scss']
|
||||
})
|
||||
export class RulesComponent {
|
||||
@Input() hasPermissionToCreateOrder: boolean;
|
||||
@Input() rules: PortfolioReportRule;
|
||||
|
||||
public constructor() {}
|
||||
|
@ -16,6 +16,8 @@ import { UserService } from '../services/user/user.service';
|
||||
export class AuthGuard implements CanActivate {
|
||||
private static PUBLIC_PAGE_ROUTES = [
|
||||
'/about',
|
||||
'/about/changelog',
|
||||
'/blog',
|
||||
'/de/blog',
|
||||
'/en/blog',
|
||||
'/p',
|
||||
|
@ -149,73 +149,23 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="hasPermissionForBlog" class="mb-5 row">
|
||||
<div class="col">
|
||||
<h3 class="mb-3 text-center" i18n>Blog</h3>
|
||||
<mat-card class="blog-container">
|
||||
<mat-card-content>
|
||||
<div class="container p-0">
|
||||
<div class="flex-nowrap mb-3 no-gutters row">
|
||||
<a
|
||||
class="d-flex w-100"
|
||||
[routerLink]="['/en', 'blog', '2021', '07', 'hello-ghostfolio']"
|
||||
>
|
||||
<div class="flex-grow-1">
|
||||
<div class="h6 m-0 text-truncate">Hello Ghostfolio</div>
|
||||
<div class="d-flex text-muted">31.07.2021</div>
|
||||
</div>
|
||||
<div class="align-items-center d-flex">
|
||||
<ion-icon
|
||||
class="chevron text-muted"
|
||||
name="chevron-forward-outline"
|
||||
size="small"
|
||||
></ion-icon>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex-nowrap no-gutters row">
|
||||
<a
|
||||
class="d-flex w-100"
|
||||
[routerLink]="['/de', 'blog', '2021', '07', 'hallo-ghostfolio']"
|
||||
>
|
||||
<div class="flex-grow-1">
|
||||
<div class="h6 m-0 text-truncate">Hallo Ghostfolio</div>
|
||||
<div class="d-flex text-muted">31.07.2021</div>
|
||||
</div>
|
||||
<div class="align-items-center d-flex">
|
||||
<ion-icon
|
||||
class="chevron text-muted"
|
||||
name="chevron-forward-outline"
|
||||
size="small"
|
||||
></ion-icon>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-5 row">
|
||||
<div class="col">
|
||||
<h3 class="mb-3 text-center" i18n>Changelog</h3>
|
||||
<mat-card class="changelog">
|
||||
<mat-card-content>
|
||||
<markdown [src]="'assets/CHANGELOG.md'"></markdown>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3 class="mb-3 text-center" i18n>License</h3>
|
||||
<mat-card>
|
||||
<mat-card-content>
|
||||
<markdown [src]="'assets/LICENSE'"></markdown>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<div *ngIf="hasPermissionForBlog" class="col-md-6 col-xs-12 my-2">
|
||||
<a class="py-2 w-100" i18n mat-stroked-button [routerLink]="['/blog']"
|
||||
>Blog</a
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="col-md-6 col-xs-12 my-2"
|
||||
[ngClass]="{ 'offset-md-3': !hasPermissionForBlog }"
|
||||
>
|
||||
<a
|
||||
class="py-2 w-100"
|
||||
i18n
|
||||
mat-stroked-button
|
||||
[routerLink]="['/about', 'changelog']"
|
||||
>Changelog & License</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -2,7 +2,6 @@ import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MarkdownModule } from 'ngx-markdown';
|
||||
|
||||
import { AboutPageRoutingModule } from './about-page-routing.module';
|
||||
import { AboutPageComponent } from './about-page.component';
|
||||
@ -13,7 +12,6 @@ import { AboutPageComponent } from './about-page.component';
|
||||
imports: [
|
||||
AboutPageRoutingModule,
|
||||
CommonModule,
|
||||
MarkdownModule.forChild(),
|
||||
MatButtonModule,
|
||||
MatCardModule
|
||||
],
|
||||
|
@ -0,0 +1,15 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
|
||||
import { ChangelogPageComponent } from './changelog-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: ChangelogPageComponent, canActivate: [AuthGuard] }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class ChangelogPageRoutingModule {}
|
@ -0,0 +1,22 @@
|
||||
import { Component, OnDestroy } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
host: { class: 'mb-5' },
|
||||
selector: 'gf-changelog-page',
|
||||
styleUrls: ['./changelog-page.scss'],
|
||||
templateUrl: './changelog-page.html'
|
||||
})
|
||||
export class ChangelogPageComponent implements OnDestroy {
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
public constructor() {}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
<div class="container">
|
||||
<div class="mb-5 row">
|
||||
<div class="col">
|
||||
<h3 class="mb-3 text-center" i18n>Changelog</h3>
|
||||
<mat-card class="changelog">
|
||||
<mat-card-content>
|
||||
<markdown [src]="'assets/CHANGELOG.md'"></markdown>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3 class="mb-3 text-center" i18n>License</h3>
|
||||
<mat-card>
|
||||
<mat-card-content>
|
||||
<markdown [src]="'assets/LICENSE'"></markdown>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,19 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MarkdownModule } from 'ngx-markdown';
|
||||
|
||||
import { ChangelogPageRoutingModule } from './changelog-page-routing.module';
|
||||
import { ChangelogPageComponent } from './changelog-page.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [ChangelogPageComponent],
|
||||
imports: [
|
||||
ChangelogPageRoutingModule,
|
||||
CommonModule,
|
||||
MarkdownModule.forChild(),
|
||||
MatCardModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class ChangelogPageModule {}
|
@ -0,0 +1,44 @@
|
||||
:host {
|
||||
color: rgb(var(--dark-primary-text));
|
||||
display: block;
|
||||
|
||||
.mat-card {
|
||||
&.changelog {
|
||||
a {
|
||||
color: rgba(var(--palette-primary-500), 1);
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
color: rgba(var(--palette-primary-300), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.changelog {
|
||||
::ng-deep {
|
||||
markdown {
|
||||
h1,
|
||||
p {
|
||||
display: none;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 18px;
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
color: rgb(var(--light-primary-text));
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
<div class="blog container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="col-md-8 offset-md-2">
|
||||
<article>
|
||||
<div class="mb-4 text-center">
|
||||
<h1 class="mb-1" i18n>Hallo Ghostfolio 👋</h1>
|
||||
@ -141,58 +141,59 @@
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<ul class="list-inline">
|
||||
<li class="h5">
|
||||
<span class="badge badge-light font-weight-normal mr-2"
|
||||
>Aktie</span
|
||||
>
|
||||
<span class="badge badge-light font-weight-normal mr-2"
|
||||
>Altersvorsorge</span
|
||||
>
|
||||
<span class="badge badge-light font-weight-normal mr-2"
|
||||
>Anlage</span
|
||||
>
|
||||
<span class="badge badge-light font-weight-normal mr-2">App</span>
|
||||
<span class="badge badge-light font-weight-normal mr-2"
|
||||
>Cryptocurrency</span
|
||||
>
|
||||
<span class="badge badge-light font-weight-normal mr-2">ETF</span>
|
||||
<span class="badge badge-light font-weight-normal mr-2"
|
||||
>Feedback</span
|
||||
>
|
||||
<span class="badge badge-light font-weight-normal mr-2"
|
||||
>Fintech</span
|
||||
>
|
||||
<span class="badge badge-light font-weight-normal mr-2"
|
||||
>Ghostfolio</span
|
||||
>
|
||||
<span class="badge badge-light font-weight-normal mr-2"
|
||||
>Investition</span
|
||||
>
|
||||
<span class="badge badge-light font-weight-normal mr-2"
|
||||
>Open Source</span
|
||||
>
|
||||
<span class="badge badge-light font-weight-normal mr-2">OSS</span>
|
||||
<span class="badge badge-light font-weight-normal mr-2"
|
||||
>Portfolio</span
|
||||
>
|
||||
<span class="badge badge-light font-weight-normal mr-2"
|
||||
>Software</span
|
||||
>
|
||||
<span class="badge badge-light font-weight-normal mr-2"
|
||||
>Strategie</span
|
||||
>
|
||||
<span class="badge badge-light font-weight-normal mr-2"
|
||||
>Trading</span
|
||||
>
|
||||
<span class="badge badge-light font-weight-normal mr-2"
|
||||
>TypeScript</span
|
||||
>
|
||||
<span class="badge badge-light font-weight-normal mr-2"
|
||||
>Vermögen</span
|
||||
>
|
||||
<span class="badge badge-light font-weight-normal mr-2"
|
||||
>Wealth Management</span
|
||||
>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Aktie</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Altersvorsorge</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Anlage</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">App</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Cryptocurrency</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Feedback</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Fintech</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Ghostfolio</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Investition</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Open Source</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">OSS</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Portfolio</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Software</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Strategie</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Trading</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">TypeScript</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Vermögen</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Wealth Management</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
@ -7,9 +7,7 @@ import { HalloGhostfolioPageComponent } from './hallo-ghostfolio-page.component'
|
||||
|
||||
@NgModule({
|
||||
declarations: [HalloGhostfolioPageComponent],
|
||||
exports: [],
|
||||
imports: [CommonModule, HalloGhostfolioPageRoutingModule, RouterModule],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class HalloGhostfolioPageModule {}
|
||||
|
@ -1,6 +1,6 @@
|
||||
<div class="blog container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="col-md-8 offset-md-2">
|
||||
<article>
|
||||
<div class="mb-4 text-center">
|
||||
<h1 class="mb-1" i18n>Hello Ghostfolio 👋</h1>
|
||||
@ -136,42 +136,44 @@
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<ul class="list-inline">
|
||||
<li class="h5">
|
||||
<span class="badge badge-light font-weight-normal mr-2"
|
||||
>Cryptocurrency</span
|
||||
>
|
||||
<span class="badge badge-light font-weight-normal mr-2">ETF</span>
|
||||
<span class="badge badge-light font-weight-normal mr-2"
|
||||
>Fintech</span
|
||||
>
|
||||
<span class="badge badge-light font-weight-normal mr-2"
|
||||
>Ghostfolio</span
|
||||
>
|
||||
<span class="badge badge-light font-weight-normal mr-2"
|
||||
>Investment</span
|
||||
>
|
||||
<span class="badge badge-light font-weight-normal mr-2"
|
||||
>Open Source</span
|
||||
>
|
||||
<span class="badge badge-light font-weight-normal mr-2">OSS</span>
|
||||
<span class="badge badge-light font-weight-normal mr-2"
|
||||
>Portfolio</span
|
||||
>
|
||||
<span class="badge badge-light font-weight-normal mr-2"
|
||||
>Software</span
|
||||
>
|
||||
<span class="badge badge-light font-weight-normal mr-2"
|
||||
>Stock</span
|
||||
>
|
||||
<span class="badge badge-light font-weight-normal mr-2"
|
||||
>Strategy</span
|
||||
>
|
||||
<span class="badge badge-light font-weight-normal mr-2"
|
||||
>Wealth</span
|
||||
>
|
||||
<span class="badge badge-light font-weight-normal mr-2"
|
||||
>Wealth Management</span
|
||||
>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Cryptocurrency</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">ETF</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Fintech</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Ghostfolio</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Investment</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Open Source</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">OSS</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Portfolio</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Software</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Stock</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Strategy</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Wealth</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Wealth Management</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
@ -7,9 +7,7 @@ import { HelloGhostfolioPageComponent } from './hello-ghostfolio-page.component'
|
||||
|
||||
@NgModule({
|
||||
declarations: [HelloGhostfolioPageComponent],
|
||||
exports: [],
|
||||
imports: [CommonModule, HelloGhostfolioPageRoutingModule, RouterModule],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class HelloGhostfolioPageModule {}
|
||||
|
@ -0,0 +1,19 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
|
||||
import { FirstMonthsInOpenSourcePageComponent } from './first-months-in-open-source-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: FirstMonthsInOpenSourcePageComponent,
|
||||
canActivate: [AuthGuard]
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class FirstMonthsInOpenSourceRoutingModule {}
|
@ -0,0 +1,9 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
host: { class: 'mb-5' },
|
||||
selector: 'gf-first-months-in-open-source-page',
|
||||
styleUrls: ['./first-months-in-open-source-page.scss'],
|
||||
templateUrl: './first-months-in-open-source-page.html'
|
||||
})
|
||||
export class FirstMonthsInOpenSourcePageComponent {}
|
@ -0,0 +1,185 @@
|
||||
<div class="blog container">
|
||||
<div class="row">
|
||||
<div class="col-md-8 offset-md-2">
|
||||
<article>
|
||||
<div class="mb-4 text-center">
|
||||
<h1 class="mb-1" i18n>
|
||||
👻 Ghostfolio –
|
||||
<span class="text-nowrap">First months in Open Source</span>
|
||||
</h1>
|
||||
<div class="text-muted"><small>05.01.2022</small></div>
|
||||
</div>
|
||||
<section class="mb-4">
|
||||
<p>
|
||||
In this article I would like to recap the first months running the
|
||||
open source project <a href="https://ghostfol.io">Ghostfolio</a>, a
|
||||
web-based personal finance management software.
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<h2 class="h4">From 1* to 100 stars on GitHub</h2>
|
||||
<p>
|
||||
When I decided to
|
||||
<a [routerLink]="['/en', 'blog', '2021', '07', 'hello-ghostfolio']"
|
||||
>publish</a
|
||||
>
|
||||
the project as
|
||||
<a href="https://github.com/ghostfolio/ghostfolio"
|
||||
>open source software</a
|
||||
>
|
||||
(OSS), I did not know what exactly to expect. In the worst case,
|
||||
nobody would care. And in the best case, the repository would be
|
||||
overrun with contributions. The truth is probably somewhere in
|
||||
between.
|
||||
</p>
|
||||
<p>
|
||||
In the beginning, it felt quite weird to develop in public where
|
||||
anyone can observe the progress. Stupid mistakes remain visible
|
||||
forever. But this feeling, fortunately, quickly settled. I believe
|
||||
the benefits like all the learning clearly outweigh the drawbacks
|
||||
when you just do it.
|
||||
</p>
|
||||
<p>
|
||||
At the end of 2021, Ghostfolio reached an important milestone:
|
||||
<a href="https://twitter.com/ghostfolio_/status/1470075774640218121"
|
||||
>100 stars</a
|
||||
>
|
||||
on GitHub. This is really exciting with almost no marketing. I am a
|
||||
technical founder, so I prefer writing code over anything else. But
|
||||
there is so much more to make this project happen: writing
|
||||
documentation, maintaining bug reports and feature requests,
|
||||
supporting users and managing the community, keeping the SaaS
|
||||
running, etc.
|
||||
</p>
|
||||
<p>
|
||||
Reaching 100 stars will not only attract very early adopters, but
|
||||
also the early adopters. At the same time, the demands and
|
||||
expectations are also increasing.
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<h2 class="h4">What is new?</h2>
|
||||
<p>
|
||||
During the last months, Ghostfolio has transformed from a one man
|
||||
project into a prospering wealth management application with 9
|
||||
contributors and counting. User feedback has directly shaped the
|
||||
direction of the product development.
|
||||
</p>
|
||||
<p>These are some selected key features:</p>
|
||||
<ul>
|
||||
<li>
|
||||
Simplified self-hosting with an
|
||||
<a href="https://hub.docker.com/r/ghostfolio/ghostfolio"
|
||||
>official Ghostfolio docker image</a
|
||||
>
|
||||
on Docker Hub
|
||||
</li>
|
||||
<li>Improved import for activities (transactions and dividend)</li>
|
||||
<li>Enriched market data for ETFs (region and industries)</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<h2 class="h4">What is coming?</h2>
|
||||
<p>Here is a brief overview of what I am planning in 2022.</p>
|
||||
<p>
|
||||
The goal remains to offer a simple and solid software to manage
|
||||
personal finances. Thus, the main focus is on the core
|
||||
functionality.
|
||||
</p>
|
||||
<p>
|
||||
My personal goal is to reach break-even with the Saas offering (<a
|
||||
[routerLink]="['/pricing']"
|
||||
>Ghostfolio Premium</a
|
||||
>) and regularly report about the progress and my learnings on this
|
||||
exciting journey.
|
||||
</p>
|
||||
<p>
|
||||
I have already started to build a
|
||||
<a
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
>community</a
|
||||
>
|
||||
of users. In the future, I would like to involve more contributors
|
||||
to further extend the functionality of Ghostfolio (e.g. with new
|
||||
reports). Get in touch with me by email at
|
||||
<a href="mailto:hi@ghostfol.io">hi@ghostfol.io</a> or on Twitter
|
||||
<a href="https://twitter.com/ghostfolio_">@ghostfolio_</a> if you
|
||||
are interested, I’m happy to discuss ideas.
|
||||
</p>
|
||||
<p>
|
||||
I would like to say thank you for all your feedback and support
|
||||
during the last months.
|
||||
</p>
|
||||
<p>
|
||||
Have a great start into the new year and happy investing<br />
|
||||
Thomas from Ghostfolio
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<p>* Pro Tip: add the first star to your own open source project</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<ul class="list-inline">
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">BuildInPublic</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Community</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Cryptocurrency</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Docker</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">ETF</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Fintech</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Ghostfolio</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Image</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Investment</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Open Source</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">OSS</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Portfolio</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Progress</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">SaaS</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Software</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Stock</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Strategy</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Wealth</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Wealth Management</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,13 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { FirstMonthsInOpenSourceRoutingModule } from './first-months-in-open-source-page-routing.module';
|
||||
import { FirstMonthsInOpenSourcePageComponent } from './first-months-in-open-source-page.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [FirstMonthsInOpenSourcePageComponent],
|
||||
imports: [CommonModule, FirstMonthsInOpenSourceRoutingModule, RouterModule],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class FirstMonthsInOpenSourcePageModule {}
|
@ -0,0 +1,3 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
15
apps/client/src/app/pages/blog/blog-page-routing.module.ts
Normal file
15
apps/client/src/app/pages/blog/blog-page-routing.module.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
|
||||
import { BlogPageComponent } from './blog-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: BlogPageComponent, canActivate: [AuthGuard] }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class BlogPageRoutingModule {}
|
22
apps/client/src/app/pages/blog/blog-page.component.ts
Normal file
22
apps/client/src/app/pages/blog/blog-page.component.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { Component, OnDestroy } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
host: { class: 'mb-5' },
|
||||
selector: 'gf-blog-page',
|
||||
styleUrls: ['./blog-page.scss'],
|
||||
templateUrl: './blog-page.html'
|
||||
})
|
||||
export class BlogPageComponent implements OnDestroy {
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
public constructor() {}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
}
|
69
apps/client/src/app/pages/blog/blog-page.html
Normal file
69
apps/client/src/app/pages/blog/blog-page.html
Normal file
@ -0,0 +1,69 @@
|
||||
<div class="container">
|
||||
<div class="mb-5 row">
|
||||
<div class="col">
|
||||
<h3 class="mb-3 text-center" i18n>Blog</h3>
|
||||
<mat-card class="blog-container">
|
||||
<mat-card-content>
|
||||
<div class="container p-0">
|
||||
<div class="flex-nowrap mb-3 no-gutters row">
|
||||
<a
|
||||
class="d-flex w-100"
|
||||
[routerLink]="['/en', 'blog', '2022', '01', 'ghostfolio-first-months-in-open-source']"
|
||||
>
|
||||
<div class="flex-grow-1">
|
||||
<div class="h6 m-0 text-truncate">
|
||||
First months in Open Source
|
||||
</div>
|
||||
<div class="d-flex text-muted">05.01.2021</div>
|
||||
</div>
|
||||
<div class="align-items-center d-flex">
|
||||
<ion-icon
|
||||
class="chevron text-muted"
|
||||
name="chevron-forward-outline"
|
||||
size="small"
|
||||
></ion-icon>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex-nowrap mb-3 no-gutters row">
|
||||
<a
|
||||
class="d-flex w-100"
|
||||
[routerLink]="['/en', 'blog', '2021', '07', 'hello-ghostfolio']"
|
||||
>
|
||||
<div class="flex-grow-1">
|
||||
<div class="h6 m-0 text-truncate">Hello Ghostfolio</div>
|
||||
<div class="d-flex text-muted">31.07.2021</div>
|
||||
</div>
|
||||
<div class="align-items-center d-flex">
|
||||
<ion-icon
|
||||
class="chevron text-muted"
|
||||
name="chevron-forward-outline"
|
||||
size="small"
|
||||
></ion-icon>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex-nowrap no-gutters row">
|
||||
<a
|
||||
class="d-flex w-100"
|
||||
[routerLink]="['/de', 'blog', '2021', '07', 'hallo-ghostfolio']"
|
||||
>
|
||||
<div class="flex-grow-1">
|
||||
<div class="h6 m-0 text-truncate">Hallo Ghostfolio</div>
|
||||
<div class="d-flex text-muted">31.07.2021</div>
|
||||
</div>
|
||||
<div class="align-items-center d-flex">
|
||||
<ion-icon
|
||||
class="chevron text-muted"
|
||||
name="chevron-forward-outline"
|
||||
size="small"
|
||||
></ion-icon>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
13
apps/client/src/app/pages/blog/blog-page.module.ts
Normal file
13
apps/client/src/app/pages/blog/blog-page.module.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
|
||||
import { BlogPageRoutingModule } from './blog-page-routing.module';
|
||||
import { BlogPageComponent } from './blog-page.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [BlogPageComponent],
|
||||
imports: [BlogPageRoutingModule, CommonModule, MatCardModule],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class BlogPageModule {}
|
8
apps/client/src/app/pages/blog/blog-page.scss
Normal file
8
apps/client/src/app/pages/blog/blog-page.scss
Normal file
@ -0,0 +1,8 @@
|
||||
:host {
|
||||
color: rgb(var(--dark-primary-text));
|
||||
display: block;
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
color: rgb(var(--light-primary-text));
|
||||
}
|
@ -1,4 +1,7 @@
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
@ -9,10 +12,11 @@ import {
|
||||
PortfolioPosition,
|
||||
User
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { ToggleOption } from '@ghostfolio/common/types';
|
||||
import { AssetClass } from '@prisma/client';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject } from 'rxjs';
|
||||
import { Subject, Subscription } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
@ -33,6 +37,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
};
|
||||
public deviceType: string;
|
||||
public hasImpersonationId: boolean;
|
||||
public hasPermissionToCreateOrder: boolean;
|
||||
public period = 'current';
|
||||
public periodOptions: ToggleOption[] = [
|
||||
{ label: 'Initial', value: 'original' },
|
||||
@ -51,6 +56,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
>;
|
||||
};
|
||||
public positionsArray: PortfolioPosition[];
|
||||
public routeQueryParams: Subscription;
|
||||
public sectors: {
|
||||
[name: string]: { name: string; value: number };
|
||||
};
|
||||
@ -69,9 +75,22 @@ export class AllocationsPageComponent 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 userService: UserService
|
||||
) {}
|
||||
) {
|
||||
this.routeQueryParams = route.queryParams
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((params) => {
|
||||
if (params['positionDetailDialog'] && params['symbol']) {
|
||||
this.openPositionDialog({
|
||||
symbol: params['symbol']
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the controller
|
||||
@ -103,6 +122,11 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
|
||||
this.hasPermissionToCreateOrder = hasPermission(
|
||||
this.user.permissions,
|
||||
permissions.createOrder
|
||||
);
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
@ -266,4 +290,32 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
private openPositionDialog({ symbol }: { symbol: string }) {
|
||||
this.userService
|
||||
.get()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((user) => {
|
||||
this.user = user;
|
||||
|
||||
const dialogRef = this.dialog.open(PositionDetailDialog, {
|
||||
autoFocus: false,
|
||||
data: {
|
||||
symbol,
|
||||
baseCurrency: this.user?.settings?.baseCurrency,
|
||||
deviceType: this.deviceType,
|
||||
locale: this.user?.settings?.locale
|
||||
},
|
||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||
});
|
||||
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
this.router.navigate(['.'], { relativeTo: this.route });
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -197,6 +197,7 @@
|
||||
<gf-positions-table
|
||||
[baseCurrency]="user?.settings?.baseCurrency"
|
||||
[deviceType]="deviceType"
|
||||
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
||||
[locale]="user?.settings?.locale"
|
||||
[positions]="positionsArray"
|
||||
></gf-positions-table>
|
||||
|
@ -2,9 +2,10 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { PortfolioPosition, User } from '@ghostfolio/common/interfaces';
|
||||
import { Position, User } from '@ghostfolio/common/interfaces';
|
||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||
import { ToggleOption } from '@ghostfolio/common/types';
|
||||
import { differenceInDays } from 'date-fns';
|
||||
import { sortBy } from 'lodash';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
@ -16,28 +17,12 @@ import { takeUntil } from 'rxjs/operators';
|
||||
templateUrl: './analysis-page.html'
|
||||
})
|
||||
export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
public accounts: {
|
||||
[symbol: string]: Pick<PortfolioPosition, 'name'> & { value: number };
|
||||
};
|
||||
public continents: {
|
||||
[code: string]: { name: string; value: number };
|
||||
};
|
||||
public countries: {
|
||||
[code: string]: { name: string; value: number };
|
||||
};
|
||||
public bottom3: Position[];
|
||||
public daysInMarket: number;
|
||||
public deviceType: string;
|
||||
public hasImpersonationId: boolean;
|
||||
public period = 'current';
|
||||
public periodOptions: ToggleOption[] = [
|
||||
{ label: 'Initial', value: 'original' },
|
||||
{ label: 'Current', value: 'current' }
|
||||
];
|
||||
public investments: InvestmentItem[];
|
||||
public portfolioPositions: { [symbol: string]: PortfolioPosition };
|
||||
public positions: { [symbol: string]: any };
|
||||
public sectors: {
|
||||
[name: string]: { name: string; value: number };
|
||||
};
|
||||
public top3: Position[];
|
||||
public user: User;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
@ -69,8 +54,29 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
this.dataService
|
||||
.fetchInvestments()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((response) => {
|
||||
this.investments = response;
|
||||
.subscribe(({ firstOrderDate, investments }) => {
|
||||
this.daysInMarket = differenceInDays(new Date(), firstOrderDate);
|
||||
this.investments = investments;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.dataService
|
||||
.fetchPositions({ range: 'max' })
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ positions }) => {
|
||||
const positionsSorted = sortBy(
|
||||
positions,
|
||||
'netPerformancePercentage'
|
||||
).reverse();
|
||||
|
||||
this.top3 = positionsSorted.slice(0, 3);
|
||||
|
||||
if (positions?.length > 3) {
|
||||
this.bottom3 = positionsSorted.slice(-3).reverse();
|
||||
} else {
|
||||
this.bottom3 = [];
|
||||
}
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
@ -5,16 +5,96 @@
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title class="align-items-center d-flex" i18n
|
||||
>Timeline</mat-card-title
|
||||
>Investment Timeline</mat-card-title
|
||||
>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<gf-investment-chart
|
||||
class="h-100"
|
||||
[daysInMarket]="daysInMarket"
|
||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||
[investments]="investments"
|
||||
></gf-investment-chart>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title class="align-items-center d-flex" i18n
|
||||
>Top 3</mat-card-title
|
||||
>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div *ngFor="let position of top3; let i = index" class="d-flex py-1">
|
||||
<div class="flex-grow-1 mr-2 text-truncate">
|
||||
{{ i + 1 }}. {{ position.name }}
|
||||
</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
position="end"
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="position.netPerformancePercentage"
|
||||
></gf-value>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<ngx-skeleton-loader
|
||||
*ngIf="!top3"
|
||||
animation="pulse"
|
||||
[theme]="{
|
||||
height: '1.5rem',
|
||||
width: '100%'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title class="align-items-center d-flex" i18n
|
||||
>Bottom 3</mat-card-title
|
||||
>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div
|
||||
*ngFor="let position of bottom3; let i = index"
|
||||
class="d-flex py-1"
|
||||
>
|
||||
<div class="flex-grow-1 mr-2 text-truncate">
|
||||
{{ i + 1 }}. {{ position.name }}
|
||||
</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
position="end"
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="position.netPerformancePercentage"
|
||||
></gf-value>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<ngx-skeleton-loader
|
||||
*ngIf="!bottom3"
|
||||
animation="pulse"
|
||||
[theme]="{
|
||||
height: '1.5rem',
|
||||
width: '100%'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -2,6 +2,8 @@ import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module';
|
||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
|
||||
import { AnalysisPageRoutingModule } from './analysis-page-routing.module';
|
||||
import { AnalysisPageComponent } from './analysis-page.component';
|
||||
@ -13,7 +15,9 @@ import { AnalysisPageComponent } from './analysis-page.component';
|
||||
AnalysisPageRoutingModule,
|
||||
CommonModule,
|
||||
GfInvestmentChartModule,
|
||||
MatCardModule
|
||||
GfValueModule,
|
||||
MatCardModule,
|
||||
NgxSkeletonLoaderModule
|
||||
],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
|
@ -3,16 +3,16 @@
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md-6">
|
||||
<mat-card class="mb-3">
|
||||
<h4 i18n>Transactions</h4>
|
||||
<p class="mb-0">Manage your transactions.</p>
|
||||
<h4 i18n>Activities</h4>
|
||||
<p class="mb-0">Manage your activities.</p>
|
||||
<p class="text-right">
|
||||
<a
|
||||
color="primary"
|
||||
i18n
|
||||
mat-button
|
||||
[routerLink]="['/portfolio', 'transactions']"
|
||||
[routerLink]="['/portfolio', 'activities']"
|
||||
>
|
||||
Open Transactions →
|
||||
<span i18n>Open Activities</span>
|
||||
<ion-icon class="ml-1" name="arrow-forward-outline"></ion-icon>
|
||||
</a>
|
||||
</p>
|
||||
</mat-card>
|
||||
@ -31,12 +31,12 @@
|
||||
<p class="text-right">
|
||||
<a
|
||||
color="primary"
|
||||
i18n
|
||||
mat-button
|
||||
[disabled]="hasPermissionForSubscription && user?.settings?.viewMode !== 'DEFAULT'"
|
||||
[routerLink]="['/portfolio', 'allocations']"
|
||||
>
|
||||
Open Allocations →
|
||||
<span i18n>Open Allocations</span>
|
||||
<ion-icon class="ml-1" name="arrow-forward-outline"></ion-icon>
|
||||
</a>
|
||||
</p>
|
||||
</mat-card>
|
||||
@ -57,12 +57,12 @@
|
||||
<p class="text-right">
|
||||
<a
|
||||
color="primary"
|
||||
i18n
|
||||
mat-button
|
||||
[disabled]="hasPermissionForSubscription && user?.settings?.viewMode !== 'DEFAULT'"
|
||||
[routerLink]="['/portfolio', 'analysis']"
|
||||
>
|
||||
Open Analysis →
|
||||
<span i18n>Open Analysis</span>
|
||||
<ion-icon class="ml-1" name="arrow-forward-outline"></ion-icon>
|
||||
</a>
|
||||
</p>
|
||||
</mat-card>
|
||||
@ -84,12 +84,12 @@
|
||||
<p class="text-right">
|
||||
<a
|
||||
color="primary"
|
||||
i18n
|
||||
mat-button
|
||||
[disabled]="hasPermissionForSubscription && user?.settings?.viewMode !== 'DEFAULT'"
|
||||
[routerLink]="['/portfolio', 'report']"
|
||||
>
|
||||
Open X-ray →
|
||||
<span i18n>Open X-ray</span>
|
||||
<ion-icon class="ml-1" name="arrow-forward-outline"></ion-icon>
|
||||
</a>
|
||||
</p>
|
||||
</mat-card>
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { PortfolioReportRule } from '@ghostfolio/common/interfaces';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { PortfolioReportRule, User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@ -14,6 +16,8 @@ export class ReportPageComponent implements OnDestroy, OnInit {
|
||||
public accountClusterRiskRules: PortfolioReportRule[];
|
||||
public currencyClusterRiskRules: PortfolioReportRule[];
|
||||
public feeRules: PortfolioReportRule[];
|
||||
public hasPermissionToCreateOrder: boolean;
|
||||
public user: User;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
@ -22,7 +26,8 @@ export class ReportPageComponent implements OnDestroy, OnInit {
|
||||
*/
|
||||
public constructor(
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService
|
||||
private dataService: DataService,
|
||||
private userService: UserService
|
||||
) {}
|
||||
|
||||
/**
|
||||
@ -41,6 +46,21 @@ export class ReportPageComponent implements OnDestroy, OnInit {
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((state) => {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
|
||||
this.hasPermissionToCreateOrder = hasPermission(
|
||||
this.user.permissions,
|
||||
permissions.createOrder
|
||||
);
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
|
@ -15,15 +15,24 @@
|
||||
</p>
|
||||
<div class="mb-4">
|
||||
<h4 class="m-0" i18n>Currency Cluster Risks</h4>
|
||||
<gf-rules [rules]="currencyClusterRiskRules"></gf-rules>
|
||||
<gf-rules
|
||||
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
||||
[rules]="currencyClusterRiskRules"
|
||||
></gf-rules>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<h4 class="m-0" i18n>Account Cluster Risks</h4>
|
||||
<gf-rules [rules]="accountClusterRiskRules"></gf-rules>
|
||||
<gf-rules
|
||||
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
||||
[rules]="accountClusterRiskRules"
|
||||
></gf-rules>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="m-0" i18n>Fees</h4>
|
||||
<gf-rules [rules]="feeRules"></gf-rules>
|
||||
<gf-rules
|
||||
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
||||
[rules]="feeRules"
|
||||
></gf-rules>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<form #addTransactionForm="ngForm" class="d-flex flex-column h-100">
|
||||
<h1 *ngIf="data.transaction.id" mat-dialog-title i18n>Update transaction</h1>
|
||||
<h1 *ngIf="!data.transaction.id" mat-dialog-title i18n>Add transaction</h1>
|
||||
<h1 *ngIf="data.transaction.id" mat-dialog-title i18n>Update activity</h1>
|
||||
<h1 *ngIf="!data.transaction.id" mat-dialog-title i18n>Add activity</h1>
|
||||
<div class="flex-grow-1" mat-dialog-content>
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
|
@ -62,7 +62,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
||||
this.routeQueryParams = route.queryParams
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((params) => {
|
||||
if (params['createDialog'] && this.hasPermissionToCreateOrder) {
|
||||
if (params['createDialog']) {
|
||||
this.openCreateTransactionDialog();
|
||||
} else if (params['editDialog']) {
|
||||
if (this.transactions) {
|
||||
@ -255,27 +255,6 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
public openPositionDialog({ symbol }: { symbol: string }): void {
|
||||
const dialogRef = this.dialog.open(PositionDetailDialog, {
|
||||
autoFocus: false,
|
||||
data: {
|
||||
symbol,
|
||||
baseCurrency: this.user?.settings?.baseCurrency,
|
||||
deviceType: this.deviceType,
|
||||
locale: this.user?.settings?.locale
|
||||
},
|
||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||
});
|
||||
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
this.router.navigate(['.'], { relativeTo: this.route });
|
||||
});
|
||||
}
|
||||
|
||||
public openUpdateTransactionDialog({
|
||||
accountId,
|
||||
currency,
|
||||
@ -412,4 +391,32 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
||||
this.router.navigate(['.'], { relativeTo: this.route });
|
||||
});
|
||||
}
|
||||
|
||||
private openPositionDialog({ symbol }: { symbol: string }) {
|
||||
this.userService
|
||||
.get()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((user) => {
|
||||
this.user = user;
|
||||
|
||||
const dialogRef = this.dialog.open(PositionDetailDialog, {
|
||||
autoFocus: false,
|
||||
data: {
|
||||
symbol,
|
||||
baseCurrency: this.user?.settings?.baseCurrency,
|
||||
deviceType: this.deviceType,
|
||||
locale: this.user?.settings?.locale
|
||||
},
|
||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||
});
|
||||
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
this.router.navigate(['.'], { relativeTo: this.route });
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +1,21 @@
|
||||
<div class="container">
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<h3 class="d-flex justify-content-center mb-3" i18n>Transactions</h3>
|
||||
<gf-transactions-table
|
||||
<h3 class="d-flex justify-content-center mb-3" i18n>Activities</h3>
|
||||
<gf-activities-table
|
||||
[activities]="transactions"
|
||||
[baseCurrency]="user?.settings?.baseCurrency"
|
||||
[deviceType]="deviceType"
|
||||
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
||||
[hasPermissionToImportOrders]="hasPermissionToImportOrders"
|
||||
[hasPermissionToCreateActivity]="hasPermissionToCreateOrder"
|
||||
[hasPermissionToImportActivities]="hasPermissionToImportOrders"
|
||||
[locale]="user?.settings?.locale"
|
||||
[showActions]="!hasImpersonationId && hasPermissionToDeleteOrder && !user.settings.isRestrictedView"
|
||||
[transactions]="transactions"
|
||||
(activityDeleted)="onDeleteTransaction($event)"
|
||||
(activityToClone)="onCloneTransaction($event)"
|
||||
(activityToUpdate)="onUpdateTransaction($event)"
|
||||
(export)="onExport()"
|
||||
(import)="onImport()"
|
||||
(transactionDeleted)="onDeleteTransaction($event)"
|
||||
(transactionToClone)="onCloneTransaction($event)"
|
||||
(transactionToUpdate)="onUpdateTransaction($event)"
|
||||
></gf-transactions-table>
|
||||
></gf-activities-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -3,8 +3,8 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { GfTransactionsTableModule } from '@ghostfolio/client/components/transactions-table/transactions-table.module';
|
||||
import { ImportTransactionsService } from '@ghostfolio/client/services/import-transactions.service';
|
||||
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
|
||||
|
||||
import { GfCreateOrUpdateTransactionDialogModule } from './create-or-update-transaction-dialog/create-or-update-transaction-dialog.module';
|
||||
import { GfImportTransactionDialogModule } from './import-transaction-dialog/import-transaction-dialog.module';
|
||||
@ -16,9 +16,9 @@ import { TransactionsPageComponent } from './transactions-page.component';
|
||||
exports: [],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfActivitiesTableModule,
|
||||
GfCreateOrUpdateTransactionDialogModule,
|
||||
GfImportTransactionDialogModule,
|
||||
GfTransactionsTableModule,
|
||||
MatButtonModule,
|
||||
MatSnackBarModule,
|
||||
RouterModule,
|
||||
|
@ -20,6 +20,19 @@ export class AdminService {
|
||||
return this.http.post<void>(`/api/admin/gather/profile-data`, {});
|
||||
}
|
||||
|
||||
public gatherProfileDataBySymbol({
|
||||
dataSource,
|
||||
symbol
|
||||
}: {
|
||||
dataSource: DataSource;
|
||||
symbol: string;
|
||||
}) {
|
||||
return this.http.post<void>(
|
||||
`/api/admin/gather/profile-data/${dataSource}/${symbol}`,
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
public gatherSymbol({
|
||||
dataSource,
|
||||
date,
|
||||
|
@ -5,7 +5,6 @@ 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 { 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';
|
||||
@ -23,13 +22,13 @@ import {
|
||||
InfoItem,
|
||||
PortfolioChart,
|
||||
PortfolioDetails,
|
||||
PortfolioInvestments,
|
||||
PortfolioPerformance,
|
||||
PortfolioPublicDetails,
|
||||
PortfolioReport,
|
||||
PortfolioSummary,
|
||||
User
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||
import { permissions } from '@ghostfolio/common/permissions';
|
||||
import { DateRange } from '@ghostfolio/common/types';
|
||||
import { DataSource, Order as OrderModel } from '@prisma/client';
|
||||
@ -124,6 +123,18 @@ export class DataService {
|
||||
return info;
|
||||
}
|
||||
|
||||
public fetchInvestments(): Observable<PortfolioInvestments> {
|
||||
return this.http.get<any>('/api/portfolio/investments').pipe(
|
||||
map((response) => {
|
||||
if (response.firstOrderDate) {
|
||||
response.firstOrderDate = parseISO(response.firstOrderDate);
|
||||
}
|
||||
|
||||
return response;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public fetchSymbolItem({
|
||||
dataSource,
|
||||
includeHistoricalData = false,
|
||||
@ -170,10 +181,6 @@ export class DataService {
|
||||
);
|
||||
}
|
||||
|
||||
public fetchInvestments() {
|
||||
return this.http.get<InvestmentItem[]>('/api/portfolio/investments');
|
||||
}
|
||||
|
||||
public fetchPortfolioDetails(aParams: { [param: string]: any }) {
|
||||
return this.http.get<PortfolioDetails>('/api/portfolio/details', {
|
||||
params: aParams
|
||||
|
@ -1,4 +1,5 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
Disallow: /p/*
|
||||
|
||||
Sitemap: https://ghostfol.io/sitemap.xml
|
||||
|
@ -6,30 +6,42 @@
|
||||
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
||||
<url>
|
||||
<loc>https://ghostfol.io</loc>
|
||||
<lastmod>2021-07-31T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-01-01T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/about</loc>
|
||||
<lastmod>2021-07-31T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-01-01T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/about/changelog</loc>
|
||||
<lastmod>2022-01-01T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/blog</loc>
|
||||
<lastmod>2022-01-01T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/blog/2021/07/hallo-ghostfolio</loc>
|
||||
<lastmod>2021-07-31T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-01-01T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/blog/2021/07/hello-ghostfolio</loc>
|
||||
<lastmod>2021-07-31T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-01-01T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/blog/2022/01/ghostfolio-first-months-in-open-source</loc>
|
||||
<lastmod>2022-01-05T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/pricing</loc>
|
||||
<lastmod>2021-07-31T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-01-01T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/register</loc>
|
||||
<lastmod>2021-07-31T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-01-01T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/resources</loc>
|
||||
<lastmod>2021-07-31T00:00:00+00:00</lastmod>
|
||||
<lastmod>2022-01-01T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
</urlset>
|
||||
|
@ -38,7 +38,7 @@ body {
|
||||
|
||||
.blog {
|
||||
a {
|
||||
color: rgba(var(--palette-primary-500), 1);
|
||||
color: rgba(var(--palette-primary-500), 1) !important;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
|
@ -10,6 +10,8 @@ export const defaultDateRangeOptions: ToggleOption[] = [
|
||||
{ label: 'Max', value: 'max' }
|
||||
];
|
||||
|
||||
export const DEMO_USER_ID = '9b112b4d-3b7d-4bad-9bdd-3b0f7b4dac2f';
|
||||
|
||||
export const ghostfolioScraperApiSymbolPrefix = '_GF_';
|
||||
export const ghostfolioCashSymbol = `${ghostfolioScraperApiSymbolPrefix}CASH`;
|
||||
export const ghostfolioFearAndGreedIndexSymbol = `${ghostfolioScraperApiSymbolPrefix}FEAR_AND_GREED_INDEX`;
|
||||
|
@ -3,8 +3,6 @@ import { getDate, getMonth, getYear, parse, subDays } from 'date-fns';
|
||||
|
||||
import { ghostfolioScraperApiSymbolPrefix } from './config';
|
||||
|
||||
export const DEMO_USER_ID = '9b112b4d-3b7d-4bad-9bdd-3b0f7b4dac2f';
|
||||
|
||||
export function capitalize(aString: string) {
|
||||
return aString.charAt(0).toUpperCase() + aString.slice(1).toLowerCase();
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import { Export } from './export.interface';
|
||||
import { InfoItem } from './info-item.interface';
|
||||
import { PortfolioChart } from './portfolio-chart.interface';
|
||||
import { PortfolioDetails } from './portfolio-details.interface';
|
||||
import { PortfolioInvestments } from './portfolio-investments.interface';
|
||||
import { PortfolioItem } from './portfolio-item.interface';
|
||||
import { PortfolioOverview } from './portfolio-overview.interface';
|
||||
import { PortfolioPerformance } from './portfolio-performance.interface';
|
||||
@ -33,6 +34,7 @@ export {
|
||||
InfoItem,
|
||||
PortfolioChart,
|
||||
PortfolioDetails,
|
||||
PortfolioInvestments,
|
||||
PortfolioItem,
|
||||
PortfolioOverview,
|
||||
PortfolioPerformance,
|
||||
|
@ -0,0 +1,6 @@
|
||||
import { InvestmentItem } from './investment-item.interface';
|
||||
|
||||
export interface PortfolioInvestments {
|
||||
firstOrderDate: Date;
|
||||
investments: InvestmentItem[];
|
||||
}
|
@ -230,14 +230,14 @@
|
||||
<button
|
||||
class="mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
[matMenuTriggerFor]="transactionsMenu"
|
||||
[matMenuTriggerFor]="activitiesMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||
</button>
|
||||
<mat-menu #transactionsMenu="matMenu" xPosition="before">
|
||||
<mat-menu #activitiesMenu="matMenu" xPosition="before">
|
||||
<button
|
||||
*ngIf="hasPermissionToImportOrders"
|
||||
*ngIf="hasPermissionToImportActivities"
|
||||
class="align-items-center d-flex"
|
||||
mat-menu-item
|
||||
(click)="onImport()"
|
||||
@ -259,19 +259,19 @@
|
||||
<button
|
||||
class="mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
[matMenuTriggerFor]="transactionMenu"
|
||||
[matMenuTriggerFor]="activityMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||
</button>
|
||||
<mat-menu #transactionMenu="matMenu" xPosition="before">
|
||||
<button i18n mat-menu-item (click)="onUpdateTransaction(element)">
|
||||
<mat-menu #activityMenu="matMenu" xPosition="before">
|
||||
<button i18n mat-menu-item (click)="onUpdateActivity(element)">
|
||||
Edit
|
||||
</button>
|
||||
<button i18n mat-menu-item (click)="onCloneTransaction(element)">
|
||||
<button i18n mat-menu-item (click)="onCloneActivity(element)">
|
||||
Clone
|
||||
</button>
|
||||
<button i18n mat-menu-item (click)="onDeleteTransaction(element.id)">
|
||||
<button i18n mat-menu-item (click)="onDeleteActivity(element.id)">
|
||||
Delete
|
||||
</button>
|
||||
</mat-menu>
|
||||
@ -305,7 +305,7 @@
|
||||
|
||||
<div
|
||||
*ngIf="
|
||||
dataSource.data.length === 0 && hasPermissionToCreateOrder && !isLoading
|
||||
dataSource.data.length === 0 && hasPermissionToCreateActivity && !isLoading
|
||||
"
|
||||
class="p-3 text-center"
|
||||
>
|
@ -30,30 +30,28 @@ const SEARCH_PLACEHOLDER = 'Search for account, currency, symbol or type...';
|
||||
const SEARCH_STRING_SEPARATOR = ',';
|
||||
|
||||
@Component({
|
||||
selector: 'gf-transactions-table',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: './transactions-table.component.html',
|
||||
styleUrls: ['./transactions-table.component.scss']
|
||||
selector: 'gf-activities-table',
|
||||
styleUrls: ['./activities-table.component.scss'],
|
||||
templateUrl: './activities-table.component.html'
|
||||
})
|
||||
export class TransactionsTableComponent
|
||||
implements OnChanges, OnDestroy, OnInit
|
||||
{
|
||||
export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
||||
@Input() activities: OrderWithAccount[];
|
||||
@Input() baseCurrency: string;
|
||||
@Input() deviceType: string;
|
||||
@Input() hasPermissionToCreateOrder: boolean;
|
||||
@Input() hasPermissionToCreateActivity: boolean;
|
||||
@Input() hasPermissionToFilter = true;
|
||||
@Input() hasPermissionToImportOrders: boolean;
|
||||
@Input() hasPermissionToImportActivities: boolean;
|
||||
@Input() hasPermissionToOpenDetails = true;
|
||||
@Input() locale: string;
|
||||
@Input() showActions: boolean;
|
||||
@Input() showSymbolColumn = true;
|
||||
@Input() transactions: OrderWithAccount[];
|
||||
|
||||
@Output() activityDeleted = new EventEmitter<string>();
|
||||
@Output() activityToClone = new EventEmitter<OrderWithAccount>();
|
||||
@Output() activityToUpdate = new EventEmitter<OrderWithAccount>();
|
||||
@Output() export = new EventEmitter<void>();
|
||||
@Output() import = new EventEmitter<void>();
|
||||
@Output() transactionDeleted = new EventEmitter<string>();
|
||||
@Output() transactionToClone = new EventEmitter<OrderWithAccount>();
|
||||
@Output() transactionToUpdate = new EventEmitter<OrderWithAccount>();
|
||||
|
||||
@ViewChild('autocomplete') matAutocomplete: MatAutocomplete;
|
||||
@ViewChild('searchInput') searchInput: ElementRef<HTMLInputElement>;
|
||||
@ -124,8 +122,6 @@ export class TransactionsTableComponent
|
||||
this.searchControl.setValue(null);
|
||||
}
|
||||
|
||||
public ngOnInit() {}
|
||||
|
||||
public ngOnChanges() {
|
||||
this.displayedColumns = [
|
||||
'count',
|
||||
@ -152,8 +148,8 @@ export class TransactionsTableComponent
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
if (this.transactions) {
|
||||
this.dataSource = new MatTableDataSource(this.transactions);
|
||||
if (this.activities) {
|
||||
this.dataSource = new MatTableDataSource(this.activities);
|
||||
this.dataSource.filterPredicate = (data, filter) => {
|
||||
const dataString = this.getFilterableValues(data)
|
||||
.join(' ')
|
||||
@ -171,13 +167,15 @@ export class TransactionsTableComponent
|
||||
}
|
||||
}
|
||||
|
||||
public onDeleteTransaction(aId: string) {
|
||||
const confirmation = confirm(
|
||||
'Do you really want to delete this transaction?'
|
||||
);
|
||||
public onCloneActivity(aActivity: OrderWithAccount) {
|
||||
this.activityToClone.emit(aActivity);
|
||||
}
|
||||
|
||||
public onDeleteActivity(aId: string) {
|
||||
const confirmation = confirm('Do you really want to delete this activity?');
|
||||
|
||||
if (confirmation) {
|
||||
this.transactionDeleted.emit(aId);
|
||||
this.activityDeleted.emit(aId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -195,12 +193,8 @@ export class TransactionsTableComponent
|
||||
});
|
||||
}
|
||||
|
||||
public onUpdateTransaction(aTransaction: OrderWithAccount) {
|
||||
this.transactionToUpdate.emit(aTransaction);
|
||||
}
|
||||
|
||||
public onCloneTransaction(aTransaction: OrderWithAccount) {
|
||||
this.transactionToClone.emit(aTransaction);
|
||||
public onUpdateActivity(aActivity: OrderWithAccount) {
|
||||
this.activityToUpdate.emit(aActivity);
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
@ -217,7 +211,7 @@ export class TransactionsTableComponent
|
||||
this.placeholder =
|
||||
lowercaseSearchKeywords.length <= 0 ? SEARCH_PLACEHOLDER : '';
|
||||
|
||||
this.allFilters = this.getSearchableFieldValues(this.transactions).filter(
|
||||
this.allFilters = this.getSearchableFieldValues(this.activities).filter(
|
||||
(item) => {
|
||||
return !lowercaseSearchKeywords.includes(item.trim().toLowerCase());
|
||||
}
|
||||
@ -226,11 +220,11 @@ export class TransactionsTableComponent
|
||||
this.filters$.next(this.allFilters);
|
||||
}
|
||||
|
||||
private getSearchableFieldValues(transactions: OrderWithAccount[]): string[] {
|
||||
private getSearchableFieldValues(activities: OrderWithAccount[]): string[] {
|
||||
const fieldValues = new Set<string>();
|
||||
|
||||
for (const transaction of transactions) {
|
||||
this.getFilterableValues(transaction, fieldValues);
|
||||
for (const activity of activities) {
|
||||
this.getFilterableValues(activity, fieldValues);
|
||||
}
|
||||
|
||||
return [...fieldValues]
|
||||
@ -255,15 +249,15 @@ export class TransactionsTableComponent
|
||||
}
|
||||
|
||||
private getFilterableValues(
|
||||
transaction: OrderWithAccount,
|
||||
activity: OrderWithAccount,
|
||||
fieldValues: Set<string> = new Set<string>()
|
||||
): string[] {
|
||||
fieldValues.add(transaction.currency);
|
||||
fieldValues.add(transaction.symbol);
|
||||
fieldValues.add(transaction.type);
|
||||
fieldValues.add(transaction.Account?.name);
|
||||
fieldValues.add(transaction.Account?.Platform?.name);
|
||||
fieldValues.add(format(transaction.date, 'yyyy'));
|
||||
fieldValues.add(activity.currency);
|
||||
fieldValues.add(activity.symbol);
|
||||
fieldValues.add(activity.type);
|
||||
fieldValues.add(activity.Account?.name);
|
||||
fieldValues.add(activity.Account?.Platform?.name);
|
||||
fieldValues.add(format(activity.date, 'yyyy'));
|
||||
|
||||
return [...fieldValues].filter((item) => {
|
||||
return item !== undefined;
|
@ -9,17 +9,17 @@ import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module';
|
||||
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
|
||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
|
||||
import { GfSymbolIconModule } from '../symbol-icon/symbol-icon.module';
|
||||
import { TransactionsTableComponent } from './transactions-table.component';
|
||||
import { ActivitiesTableComponent } from './activities-table.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [TransactionsTableComponent],
|
||||
exports: [TransactionsTableComponent],
|
||||
declarations: [ActivitiesTableComponent],
|
||||
exports: [ActivitiesTableComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfNoTransactionsInfoModule,
|
||||
@ -37,7 +37,6 @@ import { TransactionsTableComponent } from './transactions-table.component';
|
||||
ReactiveFormsModule,
|
||||
RouterModule
|
||||
],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfTransactionsTableModule {}
|
||||
export class GfActivitiesTableModule {}
|
@ -5,10 +5,10 @@
|
||||
<a
|
||||
class="align-items-center justify-content-center"
|
||||
color="primary"
|
||||
[routerLink]="['/portfolio', 'transactions']"
|
||||
[routerLink]="['/portfolio', 'activities']"
|
||||
[queryParams]="{ createDialog: true }"
|
||||
mat-button
|
||||
>
|
||||
<span i18n>Time to add your first transaction.</span>
|
||||
<span i18n>Time to add your first activity.</span>
|
||||
</a>
|
||||
</div>
|
||||
|
60
package.json
60
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ghostfolio",
|
||||
"version": "1.98.0",
|
||||
"version": "1.101.0",
|
||||
"homepage": "https://ghostfol.io",
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@ -21,7 +21,7 @@
|
||||
"database:gui": "prisma studio",
|
||||
"database:migrate": "prisma migrate deploy",
|
||||
"database:push": "prisma db push",
|
||||
"database:seed": "prisma db seed --preview-feature",
|
||||
"database:seed": "prisma db seed",
|
||||
"database:setup": "yarn database:push && yarn database:seed",
|
||||
"dep-graph": "nx dep-graph",
|
||||
"e2e": "ng e2e",
|
||||
@ -48,16 +48,16 @@
|
||||
"workspace-generator": "nx workspace-generator"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"@angular/animations": "13.1.1",
|
||||
"@angular/cdk": "13.1.1",
|
||||
"@angular/common": "13.1.1",
|
||||
"@angular/compiler": "13.1.1",
|
||||
"@angular/core": "13.1.1",
|
||||
"@angular/forms": "13.1.1",
|
||||
"@angular/material": "13.1.1",
|
||||
"@angular/platform-browser": "13.1.1",
|
||||
"@angular/platform-browser-dynamic": "13.1.1",
|
||||
"@angular/router": "13.1.1",
|
||||
"@codewithdan/observable-store": "2.2.11",
|
||||
"@dinero.js/currencies": "2.0.0-alpha.8",
|
||||
"@nestjs/common": "8.2.3",
|
||||
@ -68,7 +68,7 @@
|
||||
"@nestjs/platform-express": "8.2.3",
|
||||
"@nestjs/schedule": "1.0.2",
|
||||
"@nestjs/serve-static": "2.2.2",
|
||||
"@nrwl/angular": "13.3.0",
|
||||
"@nrwl/angular": "13.4.1",
|
||||
"@prisma/client": "3.7.0",
|
||||
"@simplewebauthn/browser": "4.1.0",
|
||||
"@simplewebauthn/server": "4.1.0",
|
||||
@ -82,7 +82,7 @@
|
||||
"bootstrap": "4.6.0",
|
||||
"cache-manager": "3.4.3",
|
||||
"cache-manager-redis-store": "2.0.0",
|
||||
"chart.js": "3.5.0",
|
||||
"chart.js": "3.7.0",
|
||||
"chartjs-adapter-date-fns": "2.0.0",
|
||||
"chartjs-plugin-datalabels": "2.0.0",
|
||||
"cheerio": "1.0.0-rc.6",
|
||||
@ -94,6 +94,7 @@
|
||||
"cryptocurrencies": "7.0.0",
|
||||
"date-fns": "2.22.1",
|
||||
"envalid": "7.2.1",
|
||||
"google-spreadsheet": "3.2.0",
|
||||
"http-status-codes": "2.2.0",
|
||||
"ionicons": "5.5.1",
|
||||
"lodash": "4.17.21",
|
||||
@ -117,25 +118,25 @@
|
||||
"zone.js": "0.11.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "13.0.3",
|
||||
"@angular-devkit/build-angular": "13.1.2",
|
||||
"@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",
|
||||
"@angular/cli": "13.1.2",
|
||||
"@angular/compiler-cli": "13.1.1",
|
||||
"@angular/language-service": "13.1.1",
|
||||
"@angular/localize": "13.1.1",
|
||||
"@nestjs/schematics": "8.0.5",
|
||||
"@nestjs/testing": "8.2.3",
|
||||
"@nrwl/cli": "13.3.0",
|
||||
"@nrwl/cypress": "13.3.0",
|
||||
"@nrwl/eslint-plugin-nx": "13.3.0",
|
||||
"@nrwl/jest": "13.3.0",
|
||||
"@nrwl/nest": "13.3.0",
|
||||
"@nrwl/node": "13.3.0",
|
||||
"@nrwl/storybook": "13.3.0",
|
||||
"@nrwl/tao": "13.3.0",
|
||||
"@nrwl/workspace": "13.3.0",
|
||||
"@nrwl/cli": "13.4.1",
|
||||
"@nrwl/cypress": "13.4.1",
|
||||
"@nrwl/eslint-plugin-nx": "13.4.1",
|
||||
"@nrwl/jest": "13.4.1",
|
||||
"@nrwl/nest": "13.4.1",
|
||||
"@nrwl/node": "13.4.1",
|
||||
"@nrwl/storybook": "13.4.1",
|
||||
"@nrwl/tao": "13.4.1",
|
||||
"@nrwl/workspace": "13.4.1",
|
||||
"@storybook/addon-essentials": "6.4.9",
|
||||
"@storybook/angular": "6.4.9",
|
||||
"@storybook/builder-webpack5": "6.4.9",
|
||||
@ -143,6 +144,7 @@
|
||||
"@types/big.js": "6.1.2",
|
||||
"@types/cache-manager": "3.4.2",
|
||||
"@types/color": "3.0.2",
|
||||
"@types/google-spreadsheet": "3.1.5",
|
||||
"@types/jest": "27.0.2",
|
||||
"@types/lodash": "4.14.174",
|
||||
"@types/node": "14.14.33",
|
||||
@ -166,7 +168,7 @@
|
||||
"rimraf": "3.0.2",
|
||||
"ts-jest": "27.0.5",
|
||||
"ts-node": "9.1.1",
|
||||
"typescript": "4.4.4"
|
||||
"typescript": "4.5.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
|
@ -0,0 +1,2 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "DataSource" ADD VALUE 'GOOGLE_SHEETS';
|
@ -184,6 +184,7 @@ enum AssetSubClass {
|
||||
enum DataSource {
|
||||
ALPHA_VANTAGE
|
||||
GHOSTFOLIO
|
||||
GOOGLE_SHEETS
|
||||
RAKUTEN
|
||||
YAHOO
|
||||
}
|
||||
|
Reference in New Issue
Block a user