Compare commits

..

33 Commits

Author SHA1 Message Date
cc16ba5dc8 Release 1.169.0 (#1077) 2022-07-14 16:31:03 +02:00
d10227bc39 Feature/add support for luna2 and songbird cryptocurrencies (#1075)
* Add LUNA2 and SGB1

* Update changelog
2022-07-14 16:28:50 +02:00
4e214c32e8 Feature/update cryptocurrencies.json 20220714 (#1074)
* Update cryptocurrencies.json

* Update changelog
2022-07-14 16:27:32 +02:00
49e2862e03 Feature/add blog post about personal finances (#1073)
* Add blog post

* Update changelog
2022-07-14 16:16:07 +02:00
34e33a2400 Feature/upgrade date fns to version 2.28.0 (#1070)
* Upgrade date-fns

* Update changelog
2022-07-11 19:39:03 +02:00
ec9bc984af Release 1.168.0 (#1071) 2022-07-10 22:25:58 +02:00
2388c494df Feature/handle currency pair inconsistency in yahoo finance service (#1069)
* Handle occasional currency pair inconsistency: GBP=X instead of USDGBP=X

* Update changelog
2022-07-10 22:24:27 +02:00
d71ab10eed Bugfix/fix content height of account detail dialog (#1068)
* Fix height

* Update changelog
2022-07-10 21:44:23 +02:00
0e0592180f Add current month (#1067) 2022-07-10 09:41:48 +02:00
60e2aff488 Extend investment timeline by month (#1066)
* Extend investment timeline grouped by month

* Update changelog
2022-07-09 21:18:05 +02:00
7b5454e7de Release 1.167.0 (#1065) 2022-07-07 21:08:32 +02:00
30835ced88 Bugfix/fix holdings for basic users (#1064)
* Fix holdings for basic users

* Update changelog
2022-07-07 21:06:12 +02:00
8897f32bc5 Clean up modules (#1063) 2022-07-07 07:04:23 +02:00
abaa6b5f27 Feature/improve create account link in live demo (#1061)
* Improve router link

* Update changelog
2022-07-06 17:49:19 +02:00
2060fcaf0b Feature/add markets to public pages (#1062)
* Add Markets to public pages

* Update changelog
2022-07-05 21:45:27 +02:00
fd2408dd62 fix: add git when building docker image (#1052) 2022-07-03 19:57:04 +02:00
31cca024f1 Feature/upgrade ngx markdown to version 14.0.1 (#1055)
* Upgrade ngx-markdown

* Update changelog
2022-07-02 10:58:46 +02:00
b535122945 Make use of demo route (#1060) 2022-07-01 20:07:49 +02:00
5113e4e3ad Release 1.166.0 (#1059) 2022-06-30 21:09:58 +02:00
35e039748f Feature/refactor demo account as route (#1058)
* Refactor demo account as route

* Update changelog
2022-06-30 21:07:35 +02:00
c6b9e0aa5b Feature/upgrade zone.js to version 0.11.6 (#1054)
* Upgrade zone.js

* Update changelog
2022-06-30 19:38:33 +02:00
b250491ca5 Feature/upgrade yahoo finance2 to version 2.3.3 (#1053)
* Upgrade yahoo-finance2 to version 2.3.3

* Update changelog
2022-06-30 08:04:39 +02:00
61e501c659 Feature/fix version of @angular/cli (#1056)
* Fix version

* Update yarn.lock
2022-06-29 21:07:55 +02:00
c0f19d56ec Feature/add account detail dialog (#1047)
* Add account detail dialog

* Update changelog
2022-06-28 21:08:34 +02:00
8e2b235b1f Feature/improve search label (#1048)
* Improve search label

* Update changelog
2022-06-28 13:33:59 +02:00
c3407e9b34 Feature/upgrade prisma to version 3.15.2 (#1046)
* Upgrade prisma to version 3.15.2

* Update changelog
2022-06-25 19:04:01 +02:00
74193e4ee2 Feature/upgrade nestjs dependencies to version 8.4.7 (#1045)
* Upgrade nestjs dependencies to version 8.4.7

* Update changelog
2022-06-25 19:03:25 +02:00
3fe8f9c882 Release 1.165.0 (#1044) 2022-06-25 17:34:06 +02:00
d130efad47 Clean up comments (#1043)
* Clean up comments
2022-06-25 17:30:43 +02:00
109f0ebd70 Feature/move positions table to holdings section (#1042)
* Move positions table to holdings section

* Update changelog
2022-06-25 14:47:20 +02:00
069ddcc6b2 Feature/add reusable premium indicator component (#1041)
* Add premium indicator component

* Update changelog
2022-06-25 12:38:15 +02:00
f7bf6e652b Feature/add icon and name to positions table (#1040)
* Add icon and name

* Update changelog
2022-06-25 11:33:59 +02:00
eb059a024a Feature/delete data in data gathering by symbol (#1039)
* Delete market data

* Update changelog
2022-06-25 11:15:01 +02:00
174 changed files with 3007 additions and 1979 deletions

View File

@ -5,6 +5,75 @@ 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.169.0 - 14.07.2022
### Added
- Added support for the cryptocurrency _Songbird_ (`SGB1-USD`)
- Added support for the cryptocurrency _Terra 2.0_ (`LUNA2-USD`)
- Added a blog post
### Changed
- Refreshed the cryptocurrencies list to support more coins by default
- Upgraded `date-fns` from version `2.22.1` to `2.28.0`
## 1.168.0 - 10.07.2022
### Added
- Extended the investment timeline grouped by month
### Changed
- Handled an occasional currency pair inconsistency in the _Yahoo Finance_ service (`GBP=X` instead of `USDGBP=X`)
### Fixed
- Fixed the content height of the account detail dialog
## 1.167.0 - 07.07.2022
### Added
- Added _Markets_ to the public pages
### Changed
- Improved the _Create Account_ link in the _Live Demo_
- Upgraded `ngx-markdown` from version `13.0.0` to `14.0.1`
### Fixed
- Fixed an issue in the _Holdings_ section for users without a subscription
## 1.166.0 - 30.06.2022
### Added
- Added an account detail dialog
### Changed
- Improved the label of the (symbol) search
- Refactored the demo account as a route (`/demo`)
- Upgraded `nestjs` from version `8.2.3` to `8.4.7`
- Upgraded `prisma` from version `3.14.0` to `3.15.2`
- Upgraded `yahoo-finance2` from version `2.3.2` to `2.3.3`
- Upgraded `zone.js` from version `0.11.4` to `0.11.6`
## 1.165.0 - 25.06.2022
### Added
- Added an icon and name column to the positions table
- Added a reusable premium indicator component
### Changed
- Moved the positions table to a dedicated section (_Holdings_)
- Changed the data gathering by symbol endpoint to delete data first
## 1.164.0 - 23.06.2022
### Added

View File

@ -12,7 +12,7 @@ COPY ./package.json package.json
COPY ./yarn.lock yarn.lock
COPY ./prisma/schema.prisma prisma/schema.prisma
RUN apk add --no-cache python3 g++ make openssl
RUN apk add --no-cache python3 g++ make openssl git
RUN yarn install
# See https://github.com/nrwl/nx/issues/6586 for further details

View File

@ -12,7 +12,7 @@
<strong>Open Source Wealth Management Software</strong>
</p>
<p>
<a href="https://ghostfol.io"><strong>Live Demo</strong></a> | <a href="https://ghostfol.io/pricing"><strong>Ghostfolio Premium</strong></a> | <a href="https://ghostfol.io/blog"><strong>Blog</strong></a> | <a href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"><strong>Slack</strong></a> | <a href="https://twitter.com/ghostfolio_"><strong>Twitter</strong></a>
<a href="https://ghostfol.io"><strong>Ghostfol.io</strong></a> | <a href="https://ghostfol.io/demo"><strong>Live Demo</strong></a> | <a href="https://ghostfol.io/pricing"><strong>Ghostfolio Premium</strong></a> | <a href="https://ghostfol.io/blog"><strong>Blog</strong></a> | <a href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"><strong>Slack</strong></a> | <a href="https://twitter.com/ghostfolio_"><strong>Twitter</strong></a>
</p>
<p>
<a href="#contributing">

View File

@ -115,7 +115,7 @@
}
],
"styles": ["apps/client/src/styles.scss"],
"scripts": ["node_modules/marked/lib/marked.js"],
"scripts": ["node_modules/marked/marked.min.js"],
"vendorChunk": true,
"extractLicenses": false,
"buildOptimizer": false,

View File

@ -7,7 +7,10 @@ import {
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
import { Accounts } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types';
import type {
AccountWithValue,
RequestWithUser
} from '@ghostfolio/common/types';
import {
Body,
Controller,
@ -123,13 +126,45 @@ export class AccountController {
@Get(':id')
@UseGuards(AuthGuard('jwt'))
public async getAccountById(@Param('id') id: string): Promise<AccountModel> {
return this.accountService.account({
id_userId: {
id,
userId: this.request.user.id
}
});
public async getAccountById(
@Headers('impersonation-id') impersonationId,
@Param('id') id: string
): Promise<AccountWithValue> {
const impersonationUserId =
await this.impersonationService.validateImpersonationId(
impersonationId,
this.request.user.id
);
let accountsWithAggregations =
await this.portfolioService.getAccountsWithAggregations(
impersonationUserId || this.request.user.id,
[{ id, type: 'ACCOUNT' }]
);
if (
impersonationUserId ||
this.userService.isRestrictedView(this.request.user)
) {
accountsWithAggregations = {
...nullifyValuesInObject(accountsWithAggregations, [
'totalBalanceInBaseCurrency',
'totalValueInBaseCurrency'
]),
accounts: nullifyValuesInObjects(accountsWithAggregations.accounts, [
'balance',
'balanceInBaseCurrency',
'convertedBalance',
'fee',
'quantity',
'unitPrice',
'value',
'valueInBaseCurrency'
])
};
}
return accountsWithAggregations.accounts[0];
}
@Post()

View File

@ -3,8 +3,7 @@ import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interc
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { PROPERTY_BENCHMARKS } from '@ghostfolio/common/config';
import { BenchmarkResponse, UniqueAsset } from '@ghostfolio/common/interfaces';
import { Controller, Get, UseGuards, UseInterceptors } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { BenchmarkService } from './benchmark.service';
@ -16,7 +15,6 @@ export class BenchmarkController {
) {}
@Get()
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getBenchmark(): Promise<BenchmarkResponse> {

View File

@ -63,6 +63,8 @@ export class InfoService {
} else {
info.fearAndGreedDataSource = ghostfolioFearAndGreedIndexDataSource;
}
globalPermissions.push(permissions.enableFearAndGreedIndex);
}
if (this.configurationService.get('ENABLE_FEATURE_IMPORT')) {

View File

@ -4,6 +4,7 @@ import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
import { Filter } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
@ -17,6 +18,7 @@ import {
Param,
Post,
Put,
Query,
UseGuards,
UseInterceptors
} from '@nestjs/common';
@ -66,8 +68,36 @@ export class OrderController {
@UseInterceptors(RedactValuesInResponseInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getAllOrders(
@Headers('impersonation-id') impersonationId
@Headers('impersonation-id') impersonationId,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('tags') filterByTags?: string
): Promise<Activities> {
const accountIds = filterByAccounts?.split(',') ?? [];
const assetClasses = filterByAssetClasses?.split(',') ?? [];
const tagIds = filterByTags?.split(',') ?? [];
const filters: Filter[] = [
...accountIds.map((accountId) => {
return <Filter>{
id: accountId,
type: 'ACCOUNT'
};
}),
...assetClasses.map((assetClass) => {
return <Filter>{
id: assetClass,
type: 'ASSET_CLASS'
};
}),
...tagIds.map((tagId) => {
return <Filter>{
id: tagId,
type: 'TAG'
};
})
];
const impersonationUserId =
await this.impersonationService.validateImpersonationId(
impersonationId,
@ -76,6 +106,7 @@ export class OrderController {
const userCurrency = this.request.user.Settings.currency;
let activities = await this.orderService.getOrders({
filters,
userCurrency,
includeDrafts: true,
userId: impersonationUserId || this.request.user.id

View File

@ -14,8 +14,11 @@ import {
format,
isAfter,
isBefore,
isSameMonth,
isSameYear,
max,
min
min,
set
} from 'date-fns';
import { first, flatten, isNumber, sortBy } from 'lodash';
@ -323,6 +326,46 @@ export class PortfolioCalculator {
});
}
public getInvestmentsByMonth(): { date: string; investment: Big }[] {
if (this.orders.length === 0) {
return [];
}
const investments = [];
let currentDate = parseDate(this.orders[0].date);
let investmentByMonth = new Big(0);
for (const [index, order] of this.orders.entries()) {
if (
isSameMonth(parseDate(order.date), currentDate) &&
isSameYear(parseDate(order.date), currentDate)
) {
investmentByMonth = investmentByMonth.plus(
order.quantity.mul(order.unitPrice).mul(this.getFactor(order.type))
);
if (index === this.orders.length - 1) {
investments.push({
date: format(set(currentDate, { date: 1 }), DATE_FORMAT),
investment: investmentByMonth
});
}
} else {
investments.push({
date: format(set(currentDate, { date: 1 }), DATE_FORMAT),
investment: investmentByMonth
});
currentDate = parseDate(order.date);
investmentByMonth = order.quantity
.mul(order.unitPrice)
.mul(this.getFactor(order.type));
}
}
return investments;
}
public async calculateTimeline(
timelineSpecification: TimelineSpecification[],
endDate: string

View File

@ -20,7 +20,12 @@ import {
PortfolioReport,
PortfolioSummary
} from '@ghostfolio/common/interfaces';
import type { DateRange, RequestWithUser } from '@ghostfolio/common/types';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
import type {
DateRange,
GroupBy,
RequestWithUser
} from '@ghostfolio/common/types';
import {
Controller,
Get,
@ -190,21 +195,35 @@ export class PortfolioController {
}
}
const isBasicUser =
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === 'Basic';
let hasDetails = true;
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
hasDetails = this.request.user.subscription.type === 'Premium';
}
for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
holdings[symbol] = {
...portfolioPosition,
assetClass: hasDetails ? portfolioPosition.assetClass : undefined,
assetSubClass: hasDetails ? portfolioPosition.assetSubClass : undefined,
countries: hasDetails ? portfolioPosition.countries : [],
currency: hasDetails ? portfolioPosition.currency : undefined,
markets: hasDetails ? portfolioPosition.markets : undefined,
sectors: hasDetails ? portfolioPosition.sectors : []
};
}
return {
accounts,
hasError,
holdings: isBasicUser ? {} : holdings
holdings
};
}
@Get('investments')
@UseGuards(AuthGuard('jwt'))
public async getInvestments(
@Headers('impersonation-id') impersonationId: string
@Headers('impersonation-id') impersonationId: string,
@Query('groupBy') groupBy?: GroupBy
): Promise<PortfolioInvestments> {
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
@ -216,9 +235,16 @@ export class PortfolioController {
);
}
let investments = await this.portfolioService.getInvestments(
impersonationId
);
let investments: InvestmentItem[];
if (groupBy === 'month') {
investments = await this.portfolioService.getInvestments(
impersonationId,
'month'
);
} else {
investments = await this.portfolioService.getInvestments(impersonationId);
}
if (
impersonationId ||
@ -340,12 +366,13 @@ export class PortfolioController {
portfolioPublicDetails.holdings[symbol] = {
allocationCurrent: portfolioPosition.value / totalValue,
countries: hasDetails ? portfolioPosition.countries : [],
currency: portfolioPosition.currency,
markets: portfolioPosition.markets,
currency: hasDetails ? portfolioPosition.currency : undefined,
markets: hasDetails ? portfolioPosition.markets : undefined,
name: portfolioPosition.name,
netPerformancePercent: portfolioPosition.netPerformancePercent,
sectors: hasDetails ? portfolioPosition.sectors : [],
symbol: portfolioPosition.symbol,
url: portfolioPosition.url,
value: portfolioPosition.value / totalValue
};
}

View File

@ -41,6 +41,7 @@ import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.in
import type {
AccountWithValue,
DateRange,
GroupBy,
Market,
OrderWithAccount,
RequestWithUser
@ -50,6 +51,7 @@ import { REQUEST } from '@nestjs/core';
import {
AssetClass,
DataSource,
Prisma,
Tag,
Type as TypeOfOrder
} from '@prisma/client';
@ -63,6 +65,7 @@ import {
max,
parse,
parseISO,
set,
setDayOfYear,
startOfDay,
subDays,
@ -100,14 +103,23 @@ export class PortfolioService {
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
}
public async getAccounts(aUserId: string): Promise<AccountWithValue[]> {
public async getAccounts(
aUserId: string,
aFilters?: Filter[]
): Promise<AccountWithValue[]> {
const where: Prisma.AccountWhereInput = { userId: aUserId };
if (aFilters?.[0].id && aFilters?.[0].type === 'ACCOUNT') {
where.id = aFilters[0].id;
}
const [accounts, details] = await Promise.all([
this.accountService.accounts({
where,
include: { Order: true, Platform: true },
orderBy: { name: 'asc' },
where: { userId: aUserId }
orderBy: { name: 'asc' }
}),
this.getDetails(aUserId, aUserId)
this.getDetails(aUserId, aUserId, undefined, aFilters)
]);
const userCurrency = this.request.user.Settings.currency;
@ -145,8 +157,11 @@ export class PortfolioService {
});
}
public async getAccountsWithAggregations(aUserId: string): Promise<Accounts> {
const accounts = await this.getAccounts(aUserId);
public async getAccountsWithAggregations(
aUserId: string,
aFilters?: Filter[]
): Promise<Accounts> {
const accounts = await this.getAccounts(aUserId, aFilters);
let totalBalanceInBaseCurrency = new Big(0);
let totalValueInBaseCurrency = new Big(0);
let transactionCount = 0;
@ -170,7 +185,8 @@ export class PortfolioService {
}
public async getInvestments(
aImpersonationId: string
aImpersonationId: string,
groupBy?: GroupBy
): Promise<InvestmentItem[]> {
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
@ -191,28 +207,57 @@ export class PortfolioService {
return [];
}
const investments = portfolioCalculator.getInvestments().map((item) => {
return {
date: item.date,
investment: item.investment.toNumber()
};
});
let investments: InvestmentItem[];
// 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());
if (groupBy === 'month') {
investments = portfolioCalculator.getInvestmentsByMonth().map((item) => {
return {
date: item.date,
investment: item.investment.toNumber()
};
});
const lastInvestment = pastInvestments[pastInvestments.length - 1];
investments.push({
date: format(new Date(), DATE_FORMAT),
investment: lastInvestment?.investment ?? 0
// Add investment of current month
const dateOfCurrentMonth = format(
set(new Date(), { date: 1 }),
DATE_FORMAT
);
const investmentOfCurrentMonth = investments.filter(({ date }) => {
return date === dateOfCurrentMonth;
});
if (investmentOfCurrentMonth.length <= 0) {
investments.push({
date: dateOfCurrentMonth,
investment: 0
});
}
} else {
investments = portfolioCalculator
.getInvestments()
.map(({ date, investment }) => {
return {
date,
investment: investment.toNumber()
};
});
// Add investment of today
const investmentOfToday = investments.filter(({ date }) => {
return date === format(new Date(), DATE_FORMAT);
});
if (investmentOfToday.length <= 0) {
const pastInvestments = investments.filter(({ date }) => {
return isBefore(parseDate(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) => {
@ -441,6 +486,7 @@ export class PortfolioService {
sectors: symbolProfile.sectors,
symbol: item.symbol,
transactionCount: item.transactionCount,
url: symbolProfile.url,
value: value.toNumber()
};
}
@ -1289,6 +1335,10 @@ export class PortfolioService {
if (filters.length === 0) {
currentAccounts = await this.accountService.getAccounts(userId);
} else if (filters.length === 1 && filters[0].type === 'ACCOUNT') {
currentAccounts = await this.accountService.accounts({
where: { id: filters[0].id }
});
} else {
const accountIds = uniq(
orders.map(({ accountId }) => {

View File

@ -46,7 +46,6 @@ export class SymbolController {
* Must be after /lookup
*/
@Get(':dataSource/:symbol')
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getSymbolData(

View File

@ -158,10 +158,6 @@ export class UserService {
let currentPermissions = getPermissions(user.role);
if (this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) {
currentPermissions.push(permissions.accessFearAndGreedIndex);
}
if (user.subscription?.type === 'Premium') {
currentPermissions.push(permissions.reportDataGlitch);
}

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,6 @@
{
"LUNA1": "Terra",
"LUNA2": "Terra",
"SGB1": "Songbird",
"UNI1": "Uniswap"
}

View File

@ -10,6 +10,7 @@ import ms from 'ms';
import { DataGatheringProcessor } from './data-gathering.processor';
import { ExchangeRateDataModule } from './exchange-rate-data.module';
import { MarketDataModule } from './market-data.module';
import { SymbolProfileModule } from './symbol-profile.module';
@Module({
@ -25,6 +26,7 @@ import { SymbolProfileModule } from './symbol-profile.module';
DataEnhancerModule,
DataProviderModule,
ExchangeRateDataModule,
MarketDataModule,
PrismaModule,
SymbolProfileModule
],

View File

@ -17,6 +17,7 @@ import { DataProviderService } from './data-provider/data-provider.service';
import { DataEnhancerInterface } from './data-provider/interfaces/data-enhancer.interface';
import { ExchangeRateDataService } from './exchange-rate-data.service';
import { IDataGatheringItem } from './interfaces/interfaces';
import { MarketDataService } from './market-data.service';
import { PrismaService } from './prisma.service';
@Injectable()
@ -28,6 +29,7 @@ export class DataGatheringService {
private readonly dataGatheringQueue: Queue,
private readonly dataProviderService: DataProviderService,
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly marketDataService: MarketDataService,
private readonly prismaService: PrismaService,
private readonly symbolProfileService: SymbolProfileService
) {}
@ -56,6 +58,8 @@ export class DataGatheringService {
}
public async gatherSymbol({ dataSource, symbol }: UniqueAsset) {
await this.marketDataService.deleteMany({ dataSource, symbol });
const symbols = (await this.getSymbolsMax()).filter((dataGatheringItem) => {
return (
dataGatheringItem.dataSource === dataSource &&

View File

@ -37,10 +37,15 @@ export class YahooFinanceService implements DataProviderInterface {
}
public convertFromYahooFinanceSymbol(aYahooFinanceSymbol: string) {
const symbol = aYahooFinanceSymbol.replace(
let symbol = aYahooFinanceSymbol.replace(
new RegExp(`-${this.baseCurrency}$`),
this.baseCurrency
);
if (symbol.includes('=X') && !symbol.includes(this.baseCurrency)) {
symbol = `${this.baseCurrency}${symbol}`;
}
return symbol.replace('=X', '');
}

View File

@ -5,9 +5,6 @@ import { getDateFormatString } from '@ghostfolio/common/helper';
import { format, parse } from 'date-fns';
export class CustomDateAdapter extends NativeDateAdapter {
/**
* @constructor
*/
public constructor(
@Inject(MAT_DATE_LOCALE) public locale: string,
@Inject(forwardRef(() => MAT_DATE_LOCALE)) matDateLocale: string,

View File

@ -59,6 +59,11 @@ const routes: Routes = [
'./pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.module'
).then((m) => m.HalloGhostfolioPageModule)
},
{
path: 'demo',
loadChildren: () =>
import('./pages/demo/demo-page.module').then((m) => m.DemoPageModule)
},
{
path: 'en/blog/2021/07/hello-ghostfolio',
loadChildren: () =>
@ -73,6 +78,13 @@ const routes: Routes = [
'./pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.module'
).then((m) => m.FirstMonthsInOpenSourcePageModule)
},
{
path: 'en/blog/2022/07/how-do-i-get-my-finances-in-order',
loadChildren: () =>
import(
'./pages/blog/2022/07/how-do-i-get-my-finances-in-order/how-do-i-get-my-finances-in-order-page.module'
).then((m) => m.HowDoIGetMyFinancesInOrderPageModule)
},
{
path: 'features',
loadChildren: () =>
@ -85,6 +97,13 @@ const routes: Routes = [
loadChildren: () =>
import('./pages/home/home-page.module').then((m) => m.HomePageModule)
},
{
path: 'markets',
loadChildren: () =>
import('./pages/markets/markets-page.module').then(
(m) => m.MarketsPageModule
)
},
{
path: 'p',
loadChildren: () =>
@ -127,6 +146,13 @@ const routes: Routes = [
(m) => m.FirePageModule
)
},
{
path: 'portfolio/holdings',
loadChildren: () =>
import('./pages/portfolio/holdings/holdings-page.module').then(
(m) => m.HoldingsPageModule
)
},
{
path: 'portfolio/report',
loadChildren: () =>

View File

@ -15,13 +15,17 @@
>
<div class="row">
<div class="col-md-8 offset-md-2 text-center">
<a *ngIf="canCreateAccount" class="text-center" [routerLink]="['/']">
<a
*ngIf="canCreateAccount"
class="text-center"
[routerLink]="['/register']"
>
<div
class="cursor-pointer d-inline-block info-message px-3 py-2"
(click)="onCreateAccount()"
>
<span i18n>You are using the Live Demo.</span>
<a class="ml-2" href="#" i18n>Create Account</a>
<span class="a ml-2" i18n>Create Account</span>
</div></a
>
<div

View File

@ -17,7 +17,7 @@
border-radius: 2rem;
font-size: 80%;
a {
.a {
color: rgba(var(--palette-primary-500), 1);
font-weight: 500;
}

View File

@ -1,6 +1,8 @@
import { Platform } from '@angular/cdk/platform';
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatChipsModule } from '@angular/material/chips';
import {
DateAdapter,
MAT_DATE_FORMATS,
@ -38,6 +40,8 @@ export function NgxStripeFactory(): string {
GfHeaderModule,
HttpClientModule,
MarkdownModule.forRoot(),
MatAutocompleteModule,
MatChipsModule,
MaterialCssVarsModule.forRoot({
darkThemeClass: 'is-dark-theme',
isAutoContrast: true,

View File

@ -10,7 +10,6 @@ import { AccessTableComponent } from './access-table.component';
declarations: [AccessTableComponent],
exports: [AccessTableComponent],
imports: [CommonModule, MatButtonModule, MatMenuModule, MatTableModule],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfPortfolioAccessTableModule {}

View File

@ -0,0 +1,7 @@
:host {
display: block;
.mat-dialog-content {
max-height: unset;
}
}

View File

@ -0,0 +1,112 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
Inject,
OnDestroy,
OnInit
} from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { downloadAsFile } from '@ghostfolio/common/helper';
import { User } from '@ghostfolio/common/interfaces';
import { OrderWithAccount } from '@ghostfolio/common/types';
import { AccountType } from '@prisma/client';
import { format, parseISO } from 'date-fns';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { AccountDetailDialogParams } from './interfaces/interfaces';
@Component({
host: { class: 'd-flex flex-column h-100' },
selector: 'gf-account-detail-dialog',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: 'account-detail-dialog.html',
styleUrls: ['./account-detail-dialog.component.scss']
})
export class AccountDetailDialog implements OnDestroy, OnInit {
public accountType: AccountType;
public name: string;
public orders: OrderWithAccount[];
public platformName: string;
public user: User;
public valueInBaseCurrency: number;
private unsubscribeSubject = new Subject<void>();
public constructor(
private changeDetectorRef: ChangeDetectorRef,
@Inject(MAT_DIALOG_DATA) public data: AccountDetailDialogParams,
private dataService: DataService,
public dialogRef: MatDialogRef<AccountDetailDialog>,
private userService: UserService
) {
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
if (state?.user) {
this.user = state.user;
this.changeDetectorRef.markForCheck();
}
});
}
public ngOnInit(): void {
this.dataService
.fetchAccount(this.data.accountId)
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ accountType, name, Platform, valueInBaseCurrency }) => {
this.accountType = accountType;
this.name = name;
this.platformName = Platform?.name;
this.valueInBaseCurrency = valueInBaseCurrency;
this.changeDetectorRef.markForCheck();
});
this.dataService
.fetchActivities({
filters: [{ id: this.data.accountId, type: 'ACCOUNT' }]
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ activities }) => {
this.orders = activities;
this.changeDetectorRef.markForCheck();
});
}
public onClose(): void {
this.dialogRef.close();
}
public onExport() {
this.dataService
.fetchExport(
this.orders.map((order) => {
return order.id;
})
)
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((data) => {
downloadAsFile({
content: data,
fileName: `ghostfolio-export-${this.name
.replace(/\s+/g, '-')
.toLowerCase()}-${format(
parseISO(data.meta.date),
'yyyyMMddHHmm'
)}.json`,
format: 'json'
});
});
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

View File

@ -0,0 +1,65 @@
<gf-dialog-header
mat-dialog-title
position="center"
[deviceType]="data.deviceType"
[title]="name"
(closeButtonClicked)="onClose()"
></gf-dialog-header>
<div class="flex-grow-1" mat-dialog-content>
<div class="container p-0">
<div class="row">
<div class="col-12 d-flex justify-content-center mb-3">
<gf-value
size="large"
[currency]="user?.settings?.baseCurrency"
[locale]="user?.settings?.locale"
[value]="valueInBaseCurrency"
></gf-value>
</div>
</div>
<div class="row">
<div class="col-6 mb-3">
<gf-value
label="Account Type"
size="medium"
[value]="accountType"
></gf-value>
</div>
<div class="col-6 mb-3">
<gf-value
label="Platform"
size="medium"
[value]="platformName"
></gf-value>
</div>
</div>
<div *ngIf="orders?.length > 0" class="row">
<div class="col mb-3">
<div class="h5 mb-0" i18n>Activities</div>
<gf-activities-table
[activities]="orders"
[baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="data.deviceType"
[hasPermissionToCreateActivity]="false"
[hasPermissionToExportActivities]="!hasImpersonationId"
[hasPermissionToFilter]="false"
[hasPermissionToImportActivities]="false"
[hasPermissionToOpenDetails]="false"
[locale]="user?.settings?.locale"
[showActions]="false"
[showSymbolColumn]="false"
(export)="onExport()"
></gf-activities-table>
</div>
</div>
</div>
</div>
<gf-dialog-footer
mat-dialog-actions
[deviceType]="data.deviceType"
(closeButtonClicked)="onClose()"
></gf-dialog-footer>

View File

@ -0,0 +1,27 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
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 { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
import { GfValueModule } from '@ghostfolio/ui/value';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { AccountDetailDialog } from './account-detail-dialog.component';
@NgModule({
declarations: [AccountDetailDialog],
imports: [
CommonModule,
GfActivitiesTableModule,
GfDialogFooterModule,
GfDialogHeaderModule,
GfValueModule,
MatButtonModule,
MatDialogModule,
NgxSkeletonLoaderModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfAccountDetailDialogModule {}

View File

@ -0,0 +1,5 @@
export interface AccountDetailDialogParams {
accountId: string;
deviceType: string;
hasImpersonationId: boolean;
}

View File

@ -65,7 +65,7 @@
<ng-container matColumnDef="transactions">
<th *matHeaderCellDef class="px-1 text-right" mat-header-cell>
<span class="d-block d-sm-none">#</span>
<span class="d-none d-sm-block" i18n>Transactions</span>
<span class="d-none d-sm-block" i18n>Activities</span>
</th>
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
<ng-container *ngIf="element.accountType === 'SECURITIES'">{{
@ -212,7 +212,12 @@
</ng-container>
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
<tr
*matRowDef="let row; columns: displayedColumns"
class="cursor-pointer"
mat-row
(click)="onOpenAccountDetailDialog(row.id)"
></tr>
<tr
*matFooterRowDef="displayedColumns"
mat-footer-row

View File

@ -9,6 +9,7 @@ import {
Output
} from '@angular/core';
import { MatTableDataSource } from '@angular/material/table';
import { Router } from '@angular/router';
import { Account as AccountModel } from '@prisma/client';
import { Subject, Subscription } from 'rxjs';
@ -39,7 +40,7 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
public constructor() {}
public constructor(private router: Router) {}
public ngOnInit() {}
@ -75,6 +76,12 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
}
}
public onOpenAccountDetailDialog(accountId: string) {
this.router.navigate([], {
queryParams: { accountId, accountDetailDialog: true }
});
}
public onUpdateAccount(aAccount: AccountModel) {
this.accountToUpdate.emit(aAccount);
}

View File

@ -25,7 +25,6 @@ import { AccountsTableComponent } from './accounts-table.component';
NgxSkeletonLoaderModule,
RouterModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfAccountsTableModule {}

View File

@ -30,9 +30,6 @@ export class AdminJobsComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef,
@ -52,9 +49,6 @@ export class AdminJobsComponent implements OnDestroy, OnInit {
});
}
/**
* Initializes the controller
*/
public ngOnInit() {
this.filterForm = this.formBuilder.group({
status: []

View File

@ -9,7 +9,6 @@ import { GfMarketDataDetailDialogModule } from './market-data-detail-dialog/mark
declarations: [AdminMarketDataDetailComponent],
exports: [AdminMarketDataDetailComponent],
imports: [CommonModule, GfLineChartModule, GfMarketDataDetailDialogModule],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfAdminMarketDataDetailModule {}

View File

@ -11,7 +11,6 @@ import { MarketDataDetailDialog } from './market-data-detail-dialog.component';
@NgModule({
declarations: [MarketDataDetailDialog],
exports: [],
imports: [
CommonModule,
FormsModule,
@ -22,7 +21,6 @@ import { MarketDataDetailDialog } from './market-data-detail-dialog.component';
MatInputModule,
ReactiveFormsModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfMarketDataDetailDialogModule {}

View File

@ -31,9 +31,6 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef,
@ -53,9 +50,6 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
});
}
/**
* Initializes the controller
*/
public ngOnInit() {
this.fetchAdminMarketData();
}

View File

@ -42,9 +42,6 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private adminService: AdminService,
private cacheService: CacheService,
@ -78,9 +75,6 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
});
}
/**
* Initializes the controller
*/
public ngOnInit() {
this.fetchAdminData();
}

View File

@ -21,9 +21,6 @@ export class AdminUsersComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
@ -38,9 +35,6 @@ export class AdminUsersComponent implements OnDestroy, OnInit {
});
}
/**
* Initializes the controller
*/
public ngOnInit() {
this.fetchAdminData();
}

View File

@ -35,11 +35,10 @@
>{{ userItem.alias || (userItem.id | slice:0:5) +
'...' }}</span
>
<ion-icon
<gf-premium-indicator
*ngIf="userItem?.subscription?.type === 'Premium'"
class="ml-1 text-muted"
name="diamond-outline"
></ion-icon>
class="ml-1"
></gf-premium-indicator>
</div>
</td>
<td class="mat-cell px-1 py-2 text-right">

View File

@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatMenuModule } from '@angular/material/menu';
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
import { GfValueModule } from '@ghostfolio/ui/value';
import { AdminUsersComponent } from './admin-users.component';
@ -9,7 +10,13 @@ import { AdminUsersComponent } from './admin-users.component';
@NgModule({
declarations: [AdminUsersComponent],
exports: [],
imports: [CommonModule, GfValueModule, MatButtonModule, MatMenuModule],
imports: [
CommonModule,
GfPremiumIndicatorModule,
GfValueModule,
MatButtonModule,
MatMenuModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfAdminUsersModule {}

View File

@ -8,7 +8,6 @@ import { DialogFooterComponent } from './dialog-footer.component';
declarations: [DialogFooterComponent],
exports: [DialogFooterComponent],
imports: [CommonModule, MatButtonModule],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfDialogFooterModule {}

View File

@ -8,7 +8,6 @@ import { DialogHeaderComponent } from './dialog-header.component';
declarations: [DialogHeaderComponent],
exports: [DialogHeaderComponent],
imports: [CommonModule, MatButtonModule],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfDialogHeaderModule {}

View File

@ -269,6 +269,18 @@
[routerLink]="['/pricing']"
>Pricing</a
>
<a
*ngIf="hasPermissionToAccessFearAndGreedIndex"
class="d-none d-sm-block mx-1"
i18n
mat-flat-button
[ngClass]="{
'font-weight-bold': currentRoute === 'markets',
'text-decoration-underline': currentRoute === 'markets'
}"
[routerLink]="['/markets']"
>Markets</a
>
<a
class="d-none d-sm-block mx-1 no-min-width px-1"
href="https://github.com/ghostfolio/ghostfolio"

View File

@ -37,6 +37,7 @@ export class HeaderComponent implements OnChanges {
public hasPermissionForSocialLogin: boolean;
public hasPermissionForSubscription: boolean;
public hasPermissionToAccessAdminControl: boolean;
public hasPermissionToAccessFearAndGreedIndex: boolean;
public impersonationId: string;
public isMenuOpen: boolean;
@ -73,6 +74,11 @@ export class HeaderComponent implements OnChanges {
this.user?.permissions,
permissions.accessAdminControl
);
this.hasPermissionToAccessFearAndGreedIndex = hasPermission(
this.info?.globalPermissions,
permissions.enableFearAndGreedIndex
);
}
public impersonateAccount(aId: string) {

View File

@ -21,7 +21,6 @@ import { HeaderComponent } from './header.component';
MatToolbarModule,
RouterModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfHeaderModule {}

View File

@ -36,9 +36,6 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
@ -81,9 +78,6 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
});
}
/**
* Initializes the controller
*/
public ngOnInit() {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;

View File

@ -11,7 +11,6 @@ import { HomeHoldingsComponent } from './home-holdings.component';
@NgModule({
declarations: [HomeHoldingsComponent],
exports: [],
imports: [
CommonModule,
GfPositionDetailDialogModule,
@ -21,7 +20,6 @@ import { HomeHoldingsComponent } from './home-holdings.component';
MatCardModule,
RouterModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfHomeHoldingsModule {}

View File

@ -30,9 +30,6 @@ export class HomeMarketComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
@ -47,52 +44,49 @@ export class HomeMarketComponent implements OnDestroy, OnInit {
if (state?.user) {
this.user = state.user;
this.hasPermissionToAccessFearAndGreedIndex = hasPermission(
this.user.permissions,
permissions.accessFearAndGreedIndex
);
if (this.hasPermissionToAccessFearAndGreedIndex) {
this.dataService
.fetchSymbolItem({
dataSource: this.info.fearAndGreedDataSource,
includeHistoricalData: this.numberOfDays,
symbol: ghostfolioFearAndGreedIndexSymbol
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ historicalData, marketPrice }) => {
this.fearAndGreedIndex = marketPrice;
this.historicalData = [
...historicalData,
{
date: resetHours(new Date()).toISOString(),
value: marketPrice
}
];
this.isLoading = false;
this.changeDetectorRef.markForCheck();
});
}
this.dataService
.fetchBenchmarks()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ benchmarks }) => {
this.benchmarks = benchmarks;
this.changeDetectorRef.markForCheck();
});
this.changeDetectorRef.markForCheck();
}
});
}
/**
* Initializes the controller
*/
public ngOnInit() {}
public ngOnInit() {
this.hasPermissionToAccessFearAndGreedIndex = hasPermission(
this.info?.globalPermissions,
permissions.enableFearAndGreedIndex
);
if (this.hasPermissionToAccessFearAndGreedIndex) {
this.dataService
.fetchSymbolItem({
dataSource: this.info.fearAndGreedDataSource,
includeHistoricalData: this.numberOfDays,
symbol: ghostfolioFearAndGreedIndexSymbol
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ historicalData, marketPrice }) => {
this.fearAndGreedIndex = marketPrice;
this.historicalData = [
...historicalData,
{
date: resetHours(new Date()).toISOString(),
value: marketPrice
}
];
this.changeDetectorRef.markForCheck();
});
}
this.dataService
.fetchBenchmarks()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ benchmarks }) => {
this.benchmarks = benchmarks;
this.isLoading = false;
this.changeDetectorRef.markForCheck();
});
}
public ngOnDestroy() {
this.unsubscribeSubject.next();

View File

@ -28,16 +28,17 @@
<div class="mb-3 row">
<div class="col-xs-12 col-md-8 offset-md-2">
<gf-benchmark
*ngFor="let benchmark of benchmarks"
class="py-2"
[benchmark]="benchmark"
[benchmarks]="benchmarks"
[locale]="user?.settings?.locale"
></gf-benchmark>
<gf-benchmark
*ngIf="!benchmarks"
class="py-2"
[benchmark]="undefined"
></gf-benchmark>
<ngx-skeleton-loader
*ngIf="isLoading"
animation="pulse"
[theme]="{
height: '1.5rem',
width: '100%'
}"
></ngx-skeleton-loader>
</div>
</div>
</div>

View File

@ -3,19 +3,20 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { GfFearAndGreedIndexModule } from '@ghostfolio/client/components/fear-and-greed-index/fear-and-greed-index.module';
import { GfBenchmarkModule } from '@ghostfolio/ui/benchmark/benchmark.module';
import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { HomeMarketComponent } from './home-market.component';
@NgModule({
declarations: [HomeMarketComponent],
exports: [],
exports: [HomeMarketComponent],
imports: [
CommonModule,
GfBenchmarkModule,
GfFearAndGreedIndexModule,
GfLineChartModule
GfLineChartModule,
NgxSkeletonLoaderModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfHomeMarketModule {}

View File

@ -42,9 +42,6 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
@ -69,9 +66,6 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
});
}
/**
* Initializes the controller
*/
public ngOnInit() {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;

View File

@ -10,7 +10,6 @@ import { HomeOverviewComponent } from './home-overview.component';
@NgModule({
declarations: [HomeOverviewComponent],
exports: [],
imports: [
CommonModule,
GfLineChartModule,
@ -19,7 +18,6 @@ import { HomeOverviewComponent } from './home-overview.component';
GfToggleModule,
RouterModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfHomeOverviewModule {}

View File

@ -21,9 +21,6 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
@ -46,9 +43,6 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
});
}
/**
* Initializes the controller
*/
public ngOnInit() {
this.impersonationStorageService
.onChangeHasImpersonation()

View File

@ -8,14 +8,12 @@ import { HomeSummaryComponent } from './home-summary.component';
@NgModule({
declarations: [HomeSummaryComponent],
exports: [],
imports: [
CommonModule,
GfPortfolioSummaryModule,
MatCardModule,
RouterModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfHomeSummaryModule {}

View File

@ -22,7 +22,10 @@ import {
transformTickToAbbreviation
} from '@ghostfolio/common/helper';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
import { GroupBy } from '@ghostfolio/common/types';
import {
BarController,
BarElement,
Chart,
LineController,
LineElement,
@ -42,6 +45,7 @@ import { addDays, isAfter, parseISO, subDays } from 'date-fns';
export class InvestmentChartComponent implements OnChanges, OnDestroy {
@Input() currency: string;
@Input() daysInMarket: number;
@Input() groupBy: GroupBy;
@Input() investments: InvestmentItem[];
@Input() isInPercent = false;
@Input() locale: string;
@ -53,6 +57,8 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
public constructor() {
Chart.register(
BarController,
BarElement,
LinearScale,
LineController,
LineElement,
@ -78,7 +84,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
private initialize() {
this.isLoading = true;
if (this.investments?.length > 0) {
if (!this.groupBy && this.investments?.length > 0) {
// Extend chart by 5% of days in market (before)
const firstItem = this.investments[0];
this.investments.unshift({
@ -102,13 +108,14 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
}
const data = {
labels: this.investments.map((position) => {
return position.date;
labels: this.investments.map((investmentItem) => {
return investmentItem.date;
}),
datasets: [
{
backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
borderColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
borderWidth: 2,
borderWidth: this.groupBy ? 0 : 2,
data: this.investments.map((position) => {
return position.investment;
}),
@ -137,6 +144,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
this.chart = new Chart(this.chartCanvas.nativeElement, {
data,
options: {
animation: false,
elements: {
line: {
tension: 0
@ -178,8 +186,10 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
grid: {
borderColor: `rgba(${getTextColor()}, 0.1)`,
color: `rgba(${getTextColor()}, 0.8)`,
display: false
display: false,
drawBorder: false
},
position: 'right',
ticks: {
callback: (value: number) => {
return transformTickToAbbreviation(value);
@ -192,12 +202,12 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
}
},
plugins: [getVerticalHoverLinePlugin(this.chartCanvas)],
type: 'line'
type: this.groupBy ? 'bar' : 'line'
});
this.isLoading = false;
}
}
this.isLoading = false;
}
private getTooltipPluginConfiguration() {

View File

@ -13,7 +13,6 @@ import { LoginWithAccessTokenDialog } from './login-with-access-token-dialog.com
@NgModule({
declarations: [LoginWithAccessTokenDialog],
exports: [],
imports: [
CommonModule,
FormsModule,
@ -26,7 +25,6 @@ import { LoginWithAccessTokenDialog } from './login-with-access-token-dialog.com
ReactiveFormsModule,
TextFieldModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class LoginWithAccessTokenDialogModule {}

View File

@ -8,7 +8,6 @@ import { PortfolioPerformanceComponent } from './portfolio-performance.component
@NgModule({
declarations: [PortfolioPerformanceComponent],
exports: [PortfolioPerformanceComponent],
imports: [CommonModule, GfValueModule, NgxSkeletonLoaderModule],
providers: []
imports: [CommonModule, GfValueModule, NgxSkeletonLoaderModule]
})
export class GfPortfolioPerformanceModule {}

View File

@ -15,7 +15,6 @@ import { PositionDetailDialog } from './position-detail-dialog.component';
@NgModule({
declarations: [PositionDetailDialog],
exports: [],
imports: [
CommonModule,
GfActivitiesTableModule,
@ -29,7 +28,6 @@ import { PositionDetailDialog } from './position-detail-dialog.component';
MatDialogModule,
NgxSkeletonLoaderModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfPositionDetailDialogModule {}

View File

@ -23,7 +23,6 @@ import { PositionComponent } from './position.component';
NgxSkeletonLoaderModule,
RouterModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfPositionModule {}

View File

@ -6,6 +6,17 @@
mat-table
[dataSource]="dataSource"
>
<ng-container matColumnDef="icon">
<th *matHeaderCellDef class="px-1" mat-header-cell></th>
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
<gf-symbol-icon
*ngIf="element.url"
[tooltip]="element.name"
[url]="element.url"
></gf-symbol-icon>
</td>
</ng-container>
<ng-container matColumnDef="symbol">
<th *matHeaderCellDef class="px-1" i18n mat-header-cell mat-sort-header>
Symbol
@ -15,6 +26,23 @@
</td>
</ng-container>
<ng-container matColumnDef="name">
<th
*matHeaderCellDef
class="d-none d-lg-table-cell px-1"
i18n
mat-header-cell
mat-sort-header
>
Name
</th>
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
<ng-container *ngIf="element.name !== element.symbol">{{
element.name
}}</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="value">
<th
*matHeaderCellDef
@ -36,48 +64,6 @@
</td>
</ng-container>
<ng-container matColumnDef="performance">
<th
*matHeaderCellDef
class="d-none d-lg-table-cell px-1 text-right"
i18n
mat-header-cell
>
Performance
</th>
<td class="d-none d-lg-table-cell px-1" mat-cell *matCellDef="let element">
<div class="d-flex justify-content-end">
<gf-value
[colorizeSign]="true"
[isPercent]="true"
[locale]="locale"
[value]="isLoading ? undefined : element.netPerformancePercent"
></gf-value>
</div>
</td>
</ng-container>
<ng-container matColumnDef="allocationInvestment">
<th
*matHeaderCellDef
class="justify-content-end px-1"
i18n
mat-header-cell
mat-sort-header
>
Initial Allocation
</th>
<td mat-cell *matCellDef="let element">
<div class="d-flex justify-content-end px-1">
<gf-value
[isPercent]="true"
[locale]="locale"
[value]="isLoading ? undefined : element.allocationInvestment"
></gf-value>
</div>
</td>
</ng-container>
<ng-container matColumnDef="allocationCurrent">
<th
*matHeaderCellDef
@ -86,7 +72,7 @@
mat-header-cell
mat-sort-header
>
Current Allocation
Allocation
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
<div class="d-flex justify-content-end">
@ -99,7 +85,28 @@
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<ng-container matColumnDef="performance">
<th
*matHeaderCellDef
class="d-none d-lg-table-cell px-1 text-right"
i18n
mat-header-cell
>
Performance
</th>
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
<div class="d-flex justify-content-end">
<gf-value
[colorizeSign]="true"
[isPercent]="true"
[locale]="locale"
[value]="isLoading ? undefined : element.netPerformancePercent"
></gf-value>
</div>
</td>
</ng-container>
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
<tr
*matRowDef="let row; columns: displayedColumns"
mat-row

View File

@ -29,6 +29,7 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
@Input() deviceType: string;
@Input() hasPermissionToShowValues = true;
@Input() locale: string;
@Input() pageSize = Number.MAX_SAFE_INTEGER;
@Input() positions: PortfolioPosition[];
@Output() transactionDeleted = new EventEmitter<string>();
@ -45,7 +46,6 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
ASSET_SUB_CLASS_EMERGENCY_FUND
];
public isLoading = true;
public pageSize = 7;
public routeQueryParams: Subscription;
private unsubscribeSubject = new Subject<void>();
@ -55,19 +55,14 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
public ngOnInit() {}
public ngOnChanges() {
this.displayedColumns = ['symbol'];
this.displayedColumns = ['icon', 'symbol', 'name'];
if (this.hasPermissionToShowValues) {
this.displayedColumns.push('value');
}
this.displayedColumns.push('performance');
if (this.hasPermissionToShowValues) {
this.displayedColumns.push('allocationInvestment');
}
this.displayedColumns.push('allocationCurrent');
this.displayedColumns.push('performance');
this.isLoading = true;

View File

@ -35,7 +35,6 @@ import { PositionsTableComponent } from './positions-table.component';
NgxSkeletonLoaderModule,
RouterModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfPositionsTableModule {}

View File

@ -15,7 +15,6 @@ import { PositionsComponent } from './positions.component';
GfPositionModule,
MatButtonModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfPositionsModule {}

View File

@ -8,7 +8,6 @@ import { RuleComponent } from './rule.component';
declarations: [RuleComponent],
exports: [RuleComponent],
imports: [CommonModule, NgxSkeletonLoaderModule],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfRuleModule {}

View File

@ -19,7 +19,6 @@ import { RulesComponent } from './rules.component';
MatButtonModule,
MatCardModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class RulesModule {}

View File

@ -7,7 +7,6 @@ import { SymbolIconComponent } from './symbol-icon.component';
declarations: [SymbolIconComponent],
exports: [SymbolIconComponent],
imports: [CommonModule],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfSymbolIconModule {}

View File

@ -8,7 +8,6 @@ import { ToggleComponent } from './toggle.component';
@NgModule({
declarations: [ToggleComponent],
exports: [ToggleComponent],
imports: [CommonModule, MatRadioModule, ReactiveFormsModule],
providers: []
imports: [CommonModule, MatRadioModule, ReactiveFormsModule]
})
export class GfToggleModule {}

View File

@ -7,7 +7,6 @@ import { WorldMapChartComponent } from './world-map-chart.component';
@NgModule({
declarations: [WorldMapChartComponent],
exports: [WorldMapChartComponent],
imports: [CommonModule, NgxSkeletonLoaderModule],
providers: []
imports: [CommonModule, NgxSkeletonLoaderModule]
})
export class GfWorldMapChartModule {}

View File

@ -5,13 +5,12 @@ import {
Router,
RouterStateSnapshot
} from '@angular/router';
import { SettingsStorageService } from '@ghostfolio/client/services/settings-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { ViewMode } from '@prisma/client';
import { EMPTY } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { SettingsStorageService } from '../services/settings-storage.service';
import { UserService } from '../services/user/user.service';
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
private static PUBLIC_PAGE_ROUTES = [
@ -20,8 +19,10 @@ export class AuthGuard implements CanActivate {
'/about/privacy-policy',
'/blog',
'/de/blog',
'/demo',
'/en/blog',
'/features',
'/markets',
'/p',
'/pricing',
'/register',
@ -35,11 +36,10 @@ export class AuthGuard implements CanActivate {
) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
if (route.queryParams?.utm_source) {
this.settingsStorageService.setSetting(
'utm_source',
route.queryParams?.utm_source
);
const utmSource = route.queryParams?.utm_source;
if (utmSource) {
this.settingsStorageService.setSetting('utm_source', utmSource);
}
return new Promise<boolean>((resolve) => {
@ -47,7 +47,10 @@ export class AuthGuard implements CanActivate {
.get()
.pipe(
catchError(() => {
if (route.queryParams?.utm_source) {
if (utmSource === 'ios') {
this.router.navigate(['/demo']);
resolve(false);
} else if (utmSource === 'trusted-web-activity') {
this.router.navigate(['/register']);
resolve(false);
} else if (

View File

@ -5,7 +5,6 @@ import {
HttpRequest
} from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { Observable } from 'rxjs';
import { ImpersonationStorageService } from '../services/impersonation-storage.service';
@ -18,7 +17,6 @@ const TOKEN_HEADER_KEY = 'Authorization';
export class AuthInterceptor implements HttpInterceptor {
public constructor(
private impersonationStorageService: ImpersonationStorageService,
private router: Router,
private tokenStorageService: TokenStorageService
) {}

View File

@ -26,9 +26,6 @@ export class AboutPageComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
@ -54,9 +51,6 @@ export class AboutPageComponent implements OnDestroy, OnInit {
this.statistics = statistics;
}
/**
* Initializes the controller
*/
public ngOnInit() {
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))

View File

@ -9,7 +9,6 @@ import { AboutPageComponent } from './about-page.component';
@NgModule({
declarations: [AboutPageComponent],
exports: [],
imports: [
AboutPageRoutingModule,
CommonModule,
@ -17,7 +16,6 @@ import { AboutPageComponent } from './about-page.component';
MatButtonModule,
MatCardModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class AboutPageModule {}

View File

@ -10,9 +10,6 @@ import { Subject } from 'rxjs';
export class ChangelogPageComponent implements OnDestroy {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor() {}
public ngOnDestroy() {

View File

@ -10,9 +10,6 @@ import { Subject } from 'rxjs';
export class PrivacyPolicyPageComponent implements OnDestroy {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor() {}
public ngOnDestroy() {

View File

@ -63,9 +63,6 @@ export class AccountPageComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
@ -145,9 +142,6 @@ export class AccountPageComponent implements OnDestroy, OnInit {
});
}
/**
* Initializes the controller
*/
public ngOnInit() {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;

View File

@ -12,18 +12,20 @@
<div class="pr-1 w-50" i18n>Alias</div>
<div class="pl-1 w-50">{{ user.alias }}</div>
</div>
<div *ngIf="user?.subscription" class="d-flex py-1">
<div
*ngIf="hasPermissionToUpdateUserSettings && user?.subscription"
class="d-flex py-1"
>
<div class="pr-1 w-50" i18n>Membership</div>
<div class="pl-1 w-50">
<div class="align-items-center d-flex mb-1">
<a [routerLink]="['/pricing']"
>{{ user?.subscription?.type }}</a
>
<ion-icon
<gf-premium-indicator
*ngIf="user?.subscription?.type === 'Premium'"
class="ml-1 text-muted"
name="diamond-outline"
></ion-icon>
class="ml-1"
></gf-premium-indicator>
</div>
<div *ngIf="user?.subscription?.type === 'Premium'">
Valid until {{ user?.subscription?.expiresAt | date:
@ -56,11 +58,11 @@
class="mr-2 my-2"
mat-stroked-button
[href]="trySubscriptionMail"
><span i18n>Try Premium</span
><ion-icon
class="ml-1 text-muted"
name="diamond-outline"
></ion-icon
><span i18n>Try Premium</span>
<gf-premium-indicator
class="d-inline-block ml-1"
[enableLink]="false"
></gf-premium-indicator
></a>
<a
class="mr-2 my-2"
@ -172,7 +174,7 @@
</div>
</div>
<div class="align-items-center d-flex mt-4 py-1">
<div class="pr-1 w-50" i18n>ID</div>
<div class="pr-1 w-50" i18n>User ID</div>
<div class="pl-1 w-50">{{ user?.id }}</div>
</div>
</mat-card-content>

View File

@ -10,6 +10,7 @@ import { MatSelectModule } from '@angular/material/select';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { RouterModule } from '@angular/router';
import { GfPortfolioAccessTableModule } from '@ghostfolio/client/components/access-table/access-table.module';
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
import { GfValueModule } from '@ghostfolio/ui/value';
import { AccountPageRoutingModule } from './account-page-routing.module';
@ -18,13 +19,13 @@ import { GfCreateOrUpdateAccessDialogModule } from './create-or-update-access-di
@NgModule({
declarations: [AccountPageComponent],
exports: [],
imports: [
AccountPageRoutingModule,
CommonModule,
FormsModule,
GfCreateOrUpdateAccessDialogModule,
GfPortfolioAccessTableModule,
GfPremiumIndicatorModule,
GfValueModule,
MatButtonModule,
MatCardModule,
@ -35,7 +36,6 @@ import { GfCreateOrUpdateAccessDialogModule } from './create-or-update-access-di
MatSlideToggleModule,
ReactiveFormsModule,
RouterModule
],
providers: []
]
})
export class AccountPageModule {}

View File

@ -10,7 +10,6 @@ import { CreateOrUpdateAccessDialog } from './create-or-update-access-dialog.com
@NgModule({
declarations: [CreateOrUpdateAccessDialog],
exports: [],
imports: [
CommonModule,
FormsModule,
@ -19,7 +18,6 @@ import { CreateOrUpdateAccessDialog } from './create-or-update-access-dialog.com
MatFormFieldModule,
MatSelectModule,
ReactiveFormsModule
],
providers: []
]
})
export class GfCreateOrUpdateAccessDialogModule {}

View File

@ -3,6 +3,8 @@ import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router';
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
import { AccountDetailDialog } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.component';
import { AccountDetailDialogParams } from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces';
import { 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';
@ -35,9 +37,6 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
@ -51,12 +50,17 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
this.route.queryParams
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((params) => {
if (params['createDialog'] && this.hasPermissionToCreateAccount) {
if (params['accountId'] && params['accountDetailDialog']) {
this.openAccountDetailDialog(params['accountId']);
} else if (
params['createDialog'] &&
this.hasPermissionToCreateAccount
) {
this.openCreateAccountDialog();
} else if (params['editDialog']) {
if (this.accounts) {
const account = this.accounts.find((account) => {
return account.id === params['transactionId'];
return account.id === params['accountId'];
});
this.openUpdateAccountDialog(account);
@ -67,9 +71,6 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
});
}
/**
* Initializes the controller
*/
public ngOnInit() {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
@ -145,7 +146,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
public onUpdateAccount(aAccount: AccountModel) {
this.router.navigate([], {
queryParams: { editDialog: true, transactionId: aAccount.id }
queryParams: { accountId: aAccount.id, editDialog: true }
});
}
@ -203,6 +204,26 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
this.unsubscribeSubject.complete();
}
private openAccountDetailDialog(aAccountId: string) {
const dialogRef = this.dialog.open(AccountDetailDialog, {
autoFocus: false,
data: <AccountDetailDialogParams>{
accountId: aAccountId,
deviceType: this.deviceType,
hasImpersonationId: this.hasImpersonationId
},
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 });
});
}
private openCreateAccountDialog(): void {
const dialogRef = this.dialog.open(CreateOrUpdateAccountDialog, {
data: {

View File

@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
import { GfAccountDetailDialogModule } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.module';
import { GfAccountsTableModule } from '@ghostfolio/client/components/accounts-table/accounts-table.module';
import { AccountsPageRoutingModule } from './accounts-page-routing.module';
@ -10,16 +11,15 @@ import { GfCreateOrUpdateAccountDialogModule } from './create-or-update-account-
@NgModule({
declarations: [AccountsPageComponent],
exports: [],
imports: [
AccountsPageRoutingModule,
CommonModule,
GfAccountDetailDialogModule,
GfAccountsTableModule,
GfCreateOrUpdateAccountDialogModule,
MatButtonModule,
RouterModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class AccountsPageModule {}

View File

@ -50,6 +50,17 @@
</mat-select>
</mat-form-field>
</div>
<div *ngIf="data.account.id">
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Account ID</mat-label>
<input
disabled
matInput
name="accountId"
[(ngModel)]="data.account.id"
/>
</mat-form-field>
</div>
</div>
<div class="justify-content-end" mat-dialog-actions>
<button i18n mat-button (click)="onCancel()">Cancel</button>

View File

@ -11,7 +11,6 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.c
@NgModule({
declarations: [CreateOrUpdateAccountDialog],
exports: [],
imports: [
CommonModule,
FormsModule,
@ -21,7 +20,6 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.c
MatInputModule,
MatSelectModule,
ReactiveFormsModule
],
providers: []
]
})
export class GfCreateOrUpdateAccountDialogModule {}

View File

@ -16,18 +16,12 @@ export class AdminPageComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(private dataService: DataService) {
const { systemMessage } = this.dataService.fetchInfo();
this.hasMessage = !!systemMessage;
}
/**
* Initializes the controller
*/
public ngOnInit() {}
public ngOnDestroy() {

View File

@ -16,9 +16,6 @@ import { takeUntil } from 'rxjs/operators';
export class AuthPageComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private route: ActivatedRoute,
private router: Router,
@ -26,9 +23,6 @@ export class AuthPageComponent implements OnDestroy, OnInit {
private tokenStorageService: TokenStorageService
) {}
/**
* Initializes the controller
*/
public ngOnInit() {
this.route.params
.pipe(takeUntil(this.unsubscribeSubject))

View File

@ -6,8 +6,6 @@ import { AuthPageComponent } from './auth-page.component';
@NgModule({
declarations: [AuthPageComponent],
exports: [],
imports: [AuthPageRoutingModule, CommonModule],
providers: []
imports: [AuthPageRoutingModule, CommonModule]
})
export class AuthPageModule {}

View File

@ -3,7 +3,7 @@
<div class="col-md-8 offset-md-2">
<article>
<div class="mb-4 text-center">
<h1 class="mb-1" i18n>Hallo Ghostfolio 👋</h1>
<h1 class="mb-1">Hallo Ghostfolio 👋</h1>
<div class="text-muted"><small>31.07.2021</small></div>
</div>
<section class="mb-4">

View File

@ -3,7 +3,7 @@
<div class="col-md-8 offset-md-2">
<article>
<div class="mb-4 text-center">
<h1 class="mb-1" i18n>Hello Ghostfolio 👋</h1>
<h1 class="mb-1">Hello Ghostfolio 👋</h1>
<div class="text-muted"><small>31.07.2021</small></div>
</div>
<section class="mb-4">

View File

@ -3,7 +3,7 @@
<div class="col-md-8 offset-md-2">
<article>
<div class="mb-4 text-center">
<h1 class="mb-1" i18n>
<h1 class="mb-1">
👻 Ghostfolio
<span class="text-nowrap">First months in Open Source</span>
</h1>

View File

@ -0,0 +1,19 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { HowDoIGetMyFinancesInOrderPageComponent } from './how-do-i-get-my-finances-in-order-page.component';
const routes: Routes = [
{
path: '',
component: HowDoIGetMyFinancesInOrderPageComponent,
canActivate: [AuthGuard]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class HowDoIGetMyFinancesInOrderRoutingModule {}

View File

@ -0,0 +1,9 @@
import { Component } from '@angular/core';
@Component({
host: { class: 'page' },
selector: 'gf-how-do-i-get-my-finances-in-order-page',
styleUrls: ['./how-do-i-get-my-finances-in-order-page.scss'],
templateUrl: './how-do-i-get-my-finances-in-order-page.html'
})
export class HowDoIGetMyFinancesInOrderPageComponent {}

View File

@ -0,0 +1,206 @@
<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">How do I get my finances in order?</h1>
<div class="text-muted"><small>14.07.2022</small></div>
</div>
<section class="mb-4">
<p>
Before you can think of
<a [routerLink]="['/resources']">long-term investing</a>, you need
to have your finances in order. Take a look at Peter's journey to
see how you can achieve it, too.
</p>
<p>
Peter enjoys life, but sometimes he overspends a bit. He realizes it
when money runs out already in the middle of the month. Then the
next few days become difficult and saving money is out of the
question. That is why he wants to plan his monthly budget in the
future.
</p>
<p>
Peter has a decent salary in his job. But as soon as the salary
arrives in his account, it melts away. In order to find out where
his money is disappearing, he has decided to plan his monthly
budget. He wants to be able to put money aside for major expenses
and set financial goals.
</p>
</section>
<section class="mb-4">
<h2 class="h4">Keeping a traditional or digital budget book</h2>
<p>
First, Peter obtains an overview of his personal finances. To do so,
he starts keeping a budget book. This can be done on paper by
listing his income and expenses for a few months, or he can create a
simple spreadsheet in Excel. In addition, many credit card providers
offer the feature within their apps of having expenses automatically
analyzed according to different categories. According to the
<a href="https://www.bfs.admin.ch/bfs/en/home.html"
>Swiss Federal Statistical Office</a
>, households in Switzerland spend around 20 percent of their
disposable income on housing and around 10 percent on groceries.
</p>
<p>
With the smartphone app, Peter has a better overview of his
financial affairs. The application assigns the bookings to
individual categories. Peter can assign specific budgets to each of
them. This way, he is always informed about how much money he can
still spend on restaurant visits in the current month, for example.
A traditional method is the so-called
<a
href="https://www.investopedia.com/envelope-budgeting-system-5208026"
>envelope method</a
>. One envelope is labeled for each category like groceries, rent or
student loans. The monthly budget is put into the envelopes in cash.
Many apps offer the same budgeting system in a more convenient,
virtual way.
</p>
</section>
<section class="mb-4">
<h2 class="h4">Planning and investing</h2>
<p>
If Peter has spent less money than planned on eating out at
restaurants, he can set aside the remaining amount. This way, he can
treat himself to something special every now and then. From now on,
he saves a fixed amount of money in a separate account ("pay
yourself first") by standing order at the beginning of the month. As
soon as there are three net monthly salaries in the account, he
invests the monthly savings amount in a passively managed global
equity fund. This grows his assets over the years and allows him to
supplement his pension later.
</p>
</section>
<section class="mb-4">
<h2 class="h4">How to achieve your financial goals?</h2>
<p>
If you follow these five actionable tips, you can reach your
financial goals easier and faster. Start with one tip and when you
implement it well, you can try the next one to ultimately have more
money at the end of the month.
</p>
<h3 class="h5">1. Visualize your goals</h3>
<p>
Start visualizing your goals. For example, hang up pictures of the
travel destination you are saving for. Imagine that you have already
achieved the goal to slowly adapt your mindset.
</p>
<h3 class="h5">2. Write off personal items</h3>
<p>
Do as a business does and write off purchases annually. For a new
car, you could set aside one-sixth of the purchase price each year.
</p>
<h3 class="h5">3. Save at the beginning of the month</h3>
<p>
Have a savings amount deducted from your account at the beginning of
the month. Then you will pay yourself first and spend less money.
</p>
<h3 class="h5">4. Follow the 50-30-20 rule</h3>
<p>
You need 50 percent of your disposable income for fixed costs. 30
percent can be spent on personal needs such as hobbies, travel or
consumer electronics. 20 percent is left for savings or to pay off
potential debts.
</p>
<h3 class="h5">5. Track your progress</h3>
<p>
If you have any money to spare, invest it in a broadly diversified,
low-cost portfolio excluding the risks of individual stocks. Track
the progress of your portfolio and net worth with
<a href="https://ghostfol.io">Ghostfolio</a>, a web-based personal
finance management software.
</p>
</section>
<section class="mb-4">
<ul class="list-inline">
<li class="list-inline-item">
<span class="badge badge-light">App</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Assets</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Budget</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Cash</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Debt</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Equity</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">Expense</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Finance</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">Fund</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">Goal</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Income</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">Money</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Net Worth</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Pension</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Personal Finance</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Planning</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">Salary</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Saving</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">Spreadsheet</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 Management</span>
</li>
</ul>
</section>
</article>
</div>
</div>
</div>

View File

@ -0,0 +1,17 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { HowDoIGetMyFinancesInOrderRoutingModule } from './how-do-i-get-my-finances-in-order-page-routing.module';
import { HowDoIGetMyFinancesInOrderPageComponent } from './how-do-i-get-my-finances-in-order-page.component';
@NgModule({
declarations: [HowDoIGetMyFinancesInOrderPageComponent],
imports: [
CommonModule,
HowDoIGetMyFinancesInOrderRoutingModule,
RouterModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class HowDoIGetMyFinancesInOrderPageModule {}

View File

@ -10,9 +10,6 @@ import { Subject } from 'rxjs';
export class BlogPageComponent implements OnDestroy {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor() {}
public ngOnDestroy() {

View File

@ -2,10 +2,36 @@
<div class="mb-5 row">
<div class="col">
<h3 class="mb-3 text-center" i18n>Blog</h3>
<mat-card class="blog-container">
<mat-card class="mb-3">
<mat-card-content>
<div class="container p-0">
<div class="flex-nowrap mb-3 no-gutters row">
<div class="flex-nowrap no-gutters row">
<a
class="d-flex w-100"
[routerLink]="['/en', 'blog', '2022', '07', 'how-do-i-get-my-finances-in-order']"
>
<div class="flex-grow-1">
<div class="h6 m-0 text-truncate">
How do I get my finances in order?
</div>
<div class="d-flex text-muted">14.07.2022</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>
<mat-card class="mb-3">
<mat-card-content>
<div class="container p-0">
<div class="flex-nowrap no-gutters row">
<a
class="d-flex w-100"
[routerLink]="['/en', 'blog', '2022', '01', 'ghostfolio-first-months-in-open-source']"
@ -25,7 +51,13 @@
</div>
</a>
</div>
<div class="flex-nowrap mb-3 no-gutters row">
</div>
</mat-card-content>
</mat-card>
<mat-card class="mb-3">
<mat-card-content>
<div class="container p-0">
<div class="flex-nowrap no-gutters row">
<a
class="d-flex w-100"
[routerLink]="['/en', 'blog', '2021', '07', 'hello-ghostfolio']"
@ -43,6 +75,12 @@
</div>
</a>
</div>
</div>
</mat-card-content>
</mat-card>
<mat-card class="mb-3">
<mat-card-content>
<div class="container p-0">
<div class="flex-nowrap no-gutters row">
<a
class="d-flex w-100"

View 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 { DemoPageComponent } from './demo-page.component';
const routes: Routes = [
{ path: '', component: DemoPageComponent, canActivate: [AuthGuard] }
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class DemoPageRoutingModule {}

View File

@ -0,0 +1,44 @@
import { Component, OnDestroy } from '@angular/core';
import { Router } from '@angular/router';
import { DataService } from '@ghostfolio/client/services/data.service';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { InfoItem } from '@ghostfolio/common/interfaces';
import { Subject } from 'rxjs';
@Component({
host: { class: 'page' },
selector: 'gf-demo-page',
templateUrl: './demo-page.html'
})
export class DemoPageComponent implements OnDestroy {
public info: InfoItem;
private unsubscribeSubject = new Subject<void>();
public constructor(
private dataService: DataService,
private router: Router,
private tokenStorageService: TokenStorageService
) {
this.info = this.dataService.fetchInfo();
}
public ngOnInit() {
const hasToken = this.tokenStorageService.getToken()?.length > 0;
if (hasToken) {
alert(
'As you are already logged in, you cannot access the demo account.'
);
} else {
this.tokenStorageService.saveToken(this.info.demoAuthToken, true);
}
this.router.navigate(['/']);
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

View File

@ -0,0 +1,12 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { DemoPageRoutingModule } from './demo-page-routing.module';
import { DemoPageComponent } from './demo-page.component';
@NgModule({
declarations: [DemoPageComponent],
imports: [CommonModule, DemoPageRoutingModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class DemoPageModule {}

View File

@ -18,9 +18,6 @@ export class FeaturesPageComponent implements OnDestroy {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
@ -29,9 +26,6 @@ export class FeaturesPageComponent implements OnDestroy {
this.info = this.dataService.fetchInfo();
}
/**
* Initializes the controller
*/
public ngOnInit() {
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))

Some files were not shown because too many files have changed in this diff Show More