Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
0578c645d1 | |||
67ae86763e | |||
266c0a9a2c | |||
a3cdb23776 | |||
e1371a8d2b | |||
448cea0b69 | |||
ad42c0bf28 | |||
f50670c7fe | |||
c0029d3b1d | |||
2518a8fd9d | |||
572dcf075a | |||
29cb83d469 | |||
cac73ac111 | |||
02cf4295a9 | |||
78b3328bf7 | |||
e0d6d9e8ca | |||
54310f2214 | |||
1fec49fbc2 |
31
CHANGELOG.md
31
CHANGELOG.md
@ -5,6 +5,37 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## 2.53.0 - 2024-02-18
|
||||
|
||||
### Added
|
||||
|
||||
- Added an accounts tab to the position detail dialog
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the usability of the holdings table
|
||||
- Refactored the query to filter activities of excluded accounts
|
||||
- Eliminated the search request to get quotes in the _EOD Historical Data_ service
|
||||
- Improved the language localization for German (`de`)
|
||||
- Upgraded `ng-extract-i18n-merge` from version `2.9.1` to `2.10.0`
|
||||
|
||||
## 2.52.0 - 2024-02-16
|
||||
|
||||
### Added
|
||||
|
||||
- Added a loading indicator to the dividend timeline on the analysis page
|
||||
- Added a loading indicator to the investment timeline on the analysis page
|
||||
- Added support for the cryptocurrency _Jupiter_ (`JUP29210-USD`)
|
||||
|
||||
### Changed
|
||||
|
||||
- Divided the content of the Frequently Asked Questions (FAQ) page into three sections: _General_, _Cloud (SaaS)_ and _Self-Hosting_
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue with the X-axis scale of the dividend timeline on the analysis page
|
||||
- Fixed an issue with the X-axis scale of the investment timeline on the analysis page
|
||||
|
||||
## 2.51.0 - 2024-02-12
|
||||
|
||||
### Changed
|
||||
|
@ -226,7 +226,7 @@ export class AdminService {
|
||||
this.prismaService.symbolProfile.count({ where })
|
||||
]);
|
||||
|
||||
let marketData = assetProfiles.map(
|
||||
let marketData: AdminMarketDataItem[] = assetProfiles.map(
|
||||
({
|
||||
_count,
|
||||
assetClass,
|
||||
|
@ -60,10 +60,6 @@ export class InfoService {
|
||||
|
||||
const globalPermissions: string[] = [];
|
||||
|
||||
if (this.configurationService.get('ENABLE_FEATURE_BLOG')) {
|
||||
globalPermissions.push(permissions.enableBlog);
|
||||
}
|
||||
|
||||
if (this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) {
|
||||
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
||||
info.fearAndGreedDataSource = encodeDataSource(
|
||||
|
@ -301,6 +301,12 @@ export class OrderService {
|
||||
});
|
||||
}
|
||||
|
||||
if (withExcludedAccounts === false) {
|
||||
where.Account = {
|
||||
NOT: { isExcluded: true }
|
||||
};
|
||||
}
|
||||
|
||||
const [orders, count] = await Promise.all([
|
||||
this.orders({
|
||||
orderBy,
|
||||
@ -322,32 +328,24 @@ export class OrderService {
|
||||
this.prismaService.order.count({ where })
|
||||
]);
|
||||
|
||||
const activities = orders
|
||||
.filter((order) => {
|
||||
return (
|
||||
withExcludedAccounts ||
|
||||
!order.Account ||
|
||||
order.Account?.isExcluded === false
|
||||
);
|
||||
})
|
||||
.map((order) => {
|
||||
const value = new Big(order.quantity).mul(order.unitPrice).toNumber();
|
||||
const activities = orders.map((order) => {
|
||||
const value = new Big(order.quantity).mul(order.unitPrice).toNumber();
|
||||
|
||||
return {
|
||||
...order,
|
||||
return {
|
||||
...order,
|
||||
value,
|
||||
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
||||
order.fee,
|
||||
order.SymbolProfile.currency,
|
||||
userCurrency
|
||||
),
|
||||
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
||||
value,
|
||||
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
||||
order.fee,
|
||||
order.SymbolProfile.currency,
|
||||
userCurrency
|
||||
),
|
||||
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
||||
value,
|
||||
order.SymbolProfile.currency,
|
||||
userCurrency
|
||||
)
|
||||
};
|
||||
});
|
||||
order.SymbolProfile.currency,
|
||||
userCurrency
|
||||
)
|
||||
};
|
||||
});
|
||||
|
||||
return { activities, count };
|
||||
}
|
||||
|
@ -107,7 +107,8 @@ describe('CurrentRateService', () => {
|
||||
|
||||
currentRateService = new CurrentRateService(
|
||||
dataProviderService,
|
||||
marketDataService
|
||||
marketDataService,
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -6,8 +6,10 @@ import {
|
||||
ResponseError,
|
||||
UniqueAsset
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { isBefore, isToday } from 'date-fns';
|
||||
import { flatten, isEmpty, uniqBy } from 'lodash';
|
||||
|
||||
@ -19,9 +21,11 @@ import { GetValuesParams } from './interfaces/get-values-params.interface';
|
||||
export class CurrentRateService {
|
||||
public constructor(
|
||||
private readonly dataProviderService: DataProviderService,
|
||||
private readonly marketDataService: MarketDataService
|
||||
private readonly marketDataService: MarketDataService,
|
||||
@Inject(REQUEST) private readonly request: RequestWithUser
|
||||
) {}
|
||||
|
||||
// TODO: Pass user instead of using this.request.user
|
||||
public async getValues({
|
||||
dataGatheringItems,
|
||||
dateQuery
|
||||
@ -40,7 +44,7 @@ export class CurrentRateService {
|
||||
if (includeToday) {
|
||||
promises.push(
|
||||
this.dataProviderService
|
||||
.getQuotes({ items: dataGatheringItems })
|
||||
.getQuotes({ items: dataGatheringItems, user: this.request?.user })
|
||||
.then((dataResultProvider) => {
|
||||
const result: GetValueObject[] = [];
|
||||
|
||||
|
@ -5,9 +5,10 @@ import {
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||
|
||||
import { Tag } from '@prisma/client';
|
||||
import { Account, Tag } from '@prisma/client';
|
||||
|
||||
export interface PortfolioPositionDetail {
|
||||
accounts: Account[];
|
||||
averagePrice: number;
|
||||
dataProviderInfo: DataProviderInfo;
|
||||
dividendInBaseCurrency: number;
|
||||
|
@ -21,7 +21,7 @@ describe('PortfolioCalculator', () => {
|
||||
let exchangeRateDataService: ExchangeRateDataService;
|
||||
|
||||
beforeEach(() => {
|
||||
currentRateService = new CurrentRateService(null, null);
|
||||
currentRateService = new CurrentRateService(null, null, null);
|
||||
|
||||
exchangeRateDataService = new ExchangeRateDataService(
|
||||
null,
|
||||
|
@ -21,7 +21,7 @@ describe('PortfolioCalculator', () => {
|
||||
let exchangeRateDataService: ExchangeRateDataService;
|
||||
|
||||
beforeEach(() => {
|
||||
currentRateService = new CurrentRateService(null, null);
|
||||
currentRateService = new CurrentRateService(null, null, null);
|
||||
|
||||
exchangeRateDataService = new ExchangeRateDataService(
|
||||
null,
|
||||
|
@ -34,7 +34,7 @@ describe('PortfolioCalculator', () => {
|
||||
let exchangeRateDataService: ExchangeRateDataService;
|
||||
|
||||
beforeEach(() => {
|
||||
currentRateService = new CurrentRateService(null, null);
|
||||
currentRateService = new CurrentRateService(null, null, null);
|
||||
|
||||
exchangeRateDataService = new ExchangeRateDataService(
|
||||
null,
|
||||
|
@ -34,7 +34,7 @@ describe('PortfolioCalculator', () => {
|
||||
let exchangeRateDataService: ExchangeRateDataService;
|
||||
|
||||
beforeEach(() => {
|
||||
currentRateService = new CurrentRateService(null, null);
|
||||
currentRateService = new CurrentRateService(null, null, null);
|
||||
|
||||
exchangeRateDataService = new ExchangeRateDataService(
|
||||
null,
|
||||
|
@ -21,7 +21,7 @@ describe('PortfolioCalculator', () => {
|
||||
let exchangeRateDataService: ExchangeRateDataService;
|
||||
|
||||
beforeEach(() => {
|
||||
currentRateService = new CurrentRateService(null, null);
|
||||
currentRateService = new CurrentRateService(null, null, null);
|
||||
|
||||
exchangeRateDataService = new ExchangeRateDataService(
|
||||
null,
|
||||
|
@ -21,7 +21,7 @@ describe('PortfolioCalculator', () => {
|
||||
let exchangeRateDataService: ExchangeRateDataService;
|
||||
|
||||
beforeEach(() => {
|
||||
currentRateService = new CurrentRateService(null, null);
|
||||
currentRateService = new CurrentRateService(null, null, null);
|
||||
|
||||
exchangeRateDataService = new ExchangeRateDataService(
|
||||
null,
|
||||
|
@ -21,7 +21,7 @@ describe('PortfolioCalculator', () => {
|
||||
let exchangeRateDataService: ExchangeRateDataService;
|
||||
|
||||
beforeEach(() => {
|
||||
currentRateService = new CurrentRateService(null, null);
|
||||
currentRateService = new CurrentRateService(null, null, null);
|
||||
|
||||
exchangeRateDataService = new ExchangeRateDataService(
|
||||
null,
|
||||
|
@ -10,7 +10,7 @@ describe('PortfolioCalculator', () => {
|
||||
let exchangeRateDataService: ExchangeRateDataService;
|
||||
|
||||
beforeEach(() => {
|
||||
currentRateService = new CurrentRateService(null, null);
|
||||
currentRateService = new CurrentRateService(null, null, null);
|
||||
|
||||
exchangeRateDataService = new ExchangeRateDataService(
|
||||
null,
|
||||
|
@ -420,7 +420,7 @@ export class PortfolioService {
|
||||
);
|
||||
|
||||
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
||||
this.dataProviderService.getQuotes({ items: dataGatheringItems }),
|
||||
this.dataProviderService.getQuotes({ user, items: dataGatheringItems }),
|
||||
this.symbolProfileService.getSymbolProfiles(dataGatheringItems)
|
||||
]);
|
||||
|
||||
@ -657,6 +657,7 @@ export class PortfolioService {
|
||||
if (orders.length <= 0) {
|
||||
return {
|
||||
tags,
|
||||
accounts: [],
|
||||
averagePrice: undefined,
|
||||
dataProviderInfo: undefined,
|
||||
dividendInBaseCurrency: undefined,
|
||||
@ -739,6 +740,13 @@ export class PortfolioService {
|
||||
transactionCount
|
||||
} = position;
|
||||
|
||||
const accounts: PortfolioPositionDetail['accounts'] = uniqBy(
|
||||
orders,
|
||||
'Account.id'
|
||||
).map(({ Account }) => {
|
||||
return Account;
|
||||
});
|
||||
|
||||
const dividendInBaseCurrency = getSum(
|
||||
orders
|
||||
.filter(({ type }) => {
|
||||
@ -812,6 +820,7 @@ export class PortfolioService {
|
||||
}
|
||||
|
||||
return {
|
||||
accounts,
|
||||
firstBuyDate,
|
||||
marketPrice,
|
||||
maxPrice,
|
||||
@ -852,6 +861,7 @@ export class PortfolioService {
|
||||
};
|
||||
} else {
|
||||
const currentData = await this.dataProviderService.getQuotes({
|
||||
user,
|
||||
items: [{ dataSource: DataSource.YAHOO, symbol: aSymbol }]
|
||||
});
|
||||
const marketPrice = currentData[aSymbol]?.marketPrice;
|
||||
@ -894,6 +904,7 @@ export class PortfolioService {
|
||||
orders,
|
||||
SymbolProfile,
|
||||
tags,
|
||||
accounts: [],
|
||||
averagePrice: 0,
|
||||
dataProviderInfo: undefined,
|
||||
dividendInBaseCurrency: 0,
|
||||
@ -929,6 +940,7 @@ export class PortfolioService {
|
||||
return type === 'SEARCH_QUERY';
|
||||
})?.id;
|
||||
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
||||
const user = await this.userService.user({ id: userId });
|
||||
|
||||
const { portfolioOrders, transactionPoints } =
|
||||
await this.getTransactionPoints({
|
||||
@ -969,7 +981,7 @@ export class PortfolioService {
|
||||
});
|
||||
|
||||
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
||||
this.dataProviderService.getQuotes({ items: dataGatheringItems }),
|
||||
this.dataProviderService.getQuotes({ user, items: dataGatheringItems }),
|
||||
this.symbolProfileService.getSymbolProfiles(
|
||||
positions.map(({ dataSource, symbol }) => {
|
||||
return { dataSource, symbol };
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"CYBER24781": "CyberConnect",
|
||||
"JUP29210": "Jupiter",
|
||||
"LUNA1": "Terra",
|
||||
"LUNA2": "Terra",
|
||||
"SGB1": "Songbird",
|
||||
|
@ -370,6 +370,14 @@
|
||||
<loc>https://ghostfol.io/en/faq</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/saas</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/self-hosting</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/features</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
|
@ -27,7 +27,6 @@ export class ConfigurationService {
|
||||
DATA_SOURCES: json({
|
||||
default: [DataSource.COINGECKO, DataSource.MANUAL, DataSource.YAHOO]
|
||||
}),
|
||||
ENABLE_FEATURE_BLOG: bool({ default: false }),
|
||||
ENABLE_FEATURE_FEAR_AND_GREED_INDEX: bool({ default: false }),
|
||||
ENABLE_FEATURE_READ_ONLY_MODE: bool({ default: false }),
|
||||
ENABLE_FEATURE_SOCIAL_LOGIN: bool({ default: false }),
|
||||
|
@ -335,11 +335,13 @@ export class DataProviderService {
|
||||
public async getQuotes({
|
||||
items,
|
||||
requestTimeout,
|
||||
useCache = true
|
||||
useCache = true,
|
||||
user
|
||||
}: {
|
||||
items: UniqueAsset[];
|
||||
requestTimeout?: number;
|
||||
useCache?: boolean;
|
||||
user?: UserWithSettings;
|
||||
}): Promise<{
|
||||
[symbol: string]: IDataProviderResponse;
|
||||
}> {
|
||||
@ -405,6 +407,14 @@ export class DataProviderService {
|
||||
)) {
|
||||
const dataProvider = this.getDataProvider(DataSource[dataSource]);
|
||||
|
||||
if (
|
||||
dataProvider.getDataProviderInfo().isPremium &&
|
||||
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
|
||||
user?.subscription.type === 'Basic'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const symbols = dataGatheringItems.map((dataGatheringItem) => {
|
||||
return dataGatheringItem.symbol;
|
||||
});
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
IDataProviderHistoricalResponse,
|
||||
IDataProviderResponse
|
||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
||||
import {
|
||||
DEFAULT_CURRENCY,
|
||||
REPLACE_NAME_PARTS
|
||||
@ -35,7 +36,8 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
private readonly URL = 'https://eodhistoricaldata.com/api';
|
||||
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService
|
||||
private readonly configurationService: ConfigurationService,
|
||||
private readonly symbolProfileService: SymbolProfileService
|
||||
) {
|
||||
this.apiKey = this.configurationService.get('API_KEY_EOD_HISTORICAL_DATA');
|
||||
}
|
||||
@ -228,27 +230,22 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
? [realTimeResponse]
|
||||
: realTimeResponse;
|
||||
|
||||
const searchResponse = await Promise.all(
|
||||
eodHistoricalDataSymbols
|
||||
.filter((symbol) => {
|
||||
return !symbol.endsWith('.FOREX');
|
||||
})
|
||||
.map((symbol) => {
|
||||
return this.search({ query: symbol });
|
||||
})
|
||||
const symbolProfiles = await this.symbolProfileService.getSymbolProfiles(
|
||||
symbols.map((symbol) => {
|
||||
return {
|
||||
symbol,
|
||||
dataSource: this.getName()
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const lookupItems = searchResponse.flat().map(({ items }) => {
|
||||
return items[0];
|
||||
});
|
||||
|
||||
response = quotes.reduce(
|
||||
(
|
||||
result: { [symbol: string]: IDataProviderResponse },
|
||||
{ close, code, timestamp }
|
||||
) => {
|
||||
const currency = lookupItems.find((lookupItem) => {
|
||||
return lookupItem.symbol === code;
|
||||
const currency = symbolProfiles.find(({ symbol }) => {
|
||||
return symbol === code;
|
||||
})?.currency;
|
||||
|
||||
if (isNumber(close)) {
|
||||
|
@ -15,7 +15,6 @@ export interface Environment extends CleanedEnvAccessors {
|
||||
DATA_SOURCE_EXCHANGE_RATES: string;
|
||||
DATA_SOURCE_IMPORT: string;
|
||||
DATA_SOURCES: string[];
|
||||
ENABLE_FEATURE_BLOG: boolean;
|
||||
ENABLE_FEATURE_FEAR_AND_GREED_INDEX: boolean;
|
||||
ENABLE_FEATURE_READ_ONLY_MODE: boolean;
|
||||
ENABLE_FEATURE_SOCIAL_LOGIN: boolean;
|
||||
|
@ -64,7 +64,7 @@
|
||||
<div class="h6 mt-2">Ghostfolio</div>
|
||||
<ul class="list-unstyled">
|
||||
<li><a i18n [routerLink]="routerLinkAbout">About</a></li>
|
||||
<li *ngIf="hasPermissionForBlog">
|
||||
<li *ngIf="hasPermissionForSubscription">
|
||||
<a i18n [routerLink]="['/blog']">Blog</a>
|
||||
</li>
|
||||
<li>
|
||||
|
@ -38,7 +38,6 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
public currentYear = new Date().getFullYear();
|
||||
public deviceType: string;
|
||||
public hasInfoMessage: boolean;
|
||||
public hasPermissionForBlog: boolean;
|
||||
public hasPermissionForStatistics: boolean;
|
||||
public hasPermissionForSubscription: boolean;
|
||||
public hasPermissionToAccessFearAndGreedIndex: boolean;
|
||||
@ -81,11 +80,6 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
this.info = this.dataService.fetchInfo();
|
||||
|
||||
this.hasPermissionForBlog = hasPermission(
|
||||
this.info?.globalPermissions,
|
||||
permissions.enableBlog
|
||||
);
|
||||
|
||||
this.hasPermissionForSubscription = hasPermission(
|
||||
this.info?.globalPermissions,
|
||||
permissions.enableSubscription
|
||||
@ -111,6 +105,7 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
|
||||
this.hasTabs =
|
||||
(this.currentRoute === this.routerLinkAbout[0].slice(1) ||
|
||||
this.currentRoute === this.routerLinkFaq[0].slice(1) ||
|
||||
this.currentRoute === 'account' ||
|
||||
this.currentRoute === 'admin' ||
|
||||
this.currentRoute === 'home' ||
|
||||
@ -120,7 +115,6 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
|
||||
this.showFooter =
|
||||
(this.currentRoute === 'blog' ||
|
||||
this.currentRoute === this.routerLinkFaq[0].slice(1) ||
|
||||
this.currentRoute === this.routerLinkFeatures[0].slice(1) ||
|
||||
this.currentRoute === this.routerLinkMarkets[0].slice(1) ||
|
||||
this.currentRoute === 'open' ||
|
||||
|
@ -77,6 +77,7 @@
|
||||
<gf-holdings-table
|
||||
[baseCurrency]="user?.settings?.baseCurrency"
|
||||
[deviceType]="data.deviceType"
|
||||
[hasPermissionToOpenDetails]="false"
|
||||
[holdings]="holdings"
|
||||
[locale]="user?.settings?.locale"
|
||||
/>
|
||||
|
@ -277,14 +277,16 @@
|
||||
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
||||
<tr
|
||||
*matRowDef="let row; columns: displayedColumns"
|
||||
class="cursor-pointer"
|
||||
mat-row
|
||||
[ngClass]="{
|
||||
'cursor-pointer': hasPermissionToOpenDetails
|
||||
}"
|
||||
(click)="onOpenAccountDetailDialog(row.id)"
|
||||
></tr>
|
||||
<tr
|
||||
*matFooterRowDef="displayedColumns"
|
||||
mat-footer-row
|
||||
[ngClass]="{ 'd-none': isLoading }"
|
||||
[ngClass]="{ 'd-none': isLoading || !showFooter }"
|
||||
></tr>
|
||||
</table>
|
||||
|
||||
|
@ -26,8 +26,14 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
|
||||
@Input() accounts: AccountModel[];
|
||||
@Input() baseCurrency: string;
|
||||
@Input() deviceType: string;
|
||||
@Input() hasPermissionToOpenDetails = true;
|
||||
@Input() locale: string;
|
||||
@Input() showActions: boolean;
|
||||
@Input() showBalance = true;
|
||||
@Input() showFooter = true;
|
||||
@Input() showTransactions = true;
|
||||
@Input() showValue = true;
|
||||
@Input() showValueInBaseCurrency = true;
|
||||
@Input() totalBalanceInBaseCurrency: number;
|
||||
@Input() totalValueInBaseCurrency: number;
|
||||
@Input() transactionCount: number;
|
||||
@ -51,17 +57,27 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
|
||||
public ngOnInit() {}
|
||||
|
||||
public ngOnChanges() {
|
||||
this.displayedColumns = [
|
||||
'status',
|
||||
'account',
|
||||
'platform',
|
||||
'transactions',
|
||||
'balance',
|
||||
'value',
|
||||
'currency',
|
||||
'valueInBaseCurrency',
|
||||
'comment'
|
||||
];
|
||||
this.displayedColumns = ['status', 'account', 'platform'];
|
||||
|
||||
if (this.showTransactions) {
|
||||
this.displayedColumns.push('transactions');
|
||||
}
|
||||
|
||||
if (this.showBalance) {
|
||||
this.displayedColumns.push('balance');
|
||||
}
|
||||
|
||||
if (this.showValue) {
|
||||
this.displayedColumns.push('value');
|
||||
}
|
||||
|
||||
this.displayedColumns.push('currency');
|
||||
|
||||
if (this.showValueInBaseCurrency) {
|
||||
this.displayedColumns.push('valueInBaseCurrency');
|
||||
}
|
||||
|
||||
this.displayedColumns.push('comment');
|
||||
|
||||
if (this.showActions) {
|
||||
this.displayedColumns.push('actions');
|
||||
@ -89,9 +105,11 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
public onOpenAccountDetailDialog(accountId: string) {
|
||||
this.router.navigate([], {
|
||||
queryParams: { accountId, accountDetailDialog: true }
|
||||
});
|
||||
if (this.hasPermissionToOpenDetails) {
|
||||
this.router.navigate([], {
|
||||
queryParams: { accountId, accountDetailDialog: true }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public onOpenComment(aComment: string) {
|
||||
|
@ -91,7 +91,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
||||
private snackBar: MatSnackBar
|
||||
) {}
|
||||
|
||||
public ngOnInit(): void {
|
||||
public ngOnInit() {
|
||||
const { benchmarks, currencies } = this.dataService.fetchInfo();
|
||||
|
||||
this.benchmarks = benchmarks;
|
||||
@ -167,7 +167,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
public onClose(): void {
|
||||
public onClose() {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
|
@ -131,50 +131,48 @@
|
||||
<ng-container
|
||||
*ngIf="assetProfile?.countries?.length > 0 || assetProfile?.sectors?.length > 0"
|
||||
>
|
||||
<ng-container
|
||||
*ngIf="assetProfile?.countries?.length === 1 && assetProfile?.sectors?.length === 1; else charts"
|
||||
>
|
||||
<div *ngIf="assetProfile?.sectors?.length === 1" class="col-6 mb-3">
|
||||
<gf-value
|
||||
i18n
|
||||
size="medium"
|
||||
[locale]="data.locale"
|
||||
[value]="assetProfile?.sectors[0].name"
|
||||
>Sector</gf-value
|
||||
>
|
||||
</div>
|
||||
<div *ngIf="assetProfile?.countries?.length === 1" class="col-6 mb-3">
|
||||
<gf-value
|
||||
i18n
|
||||
size="medium"
|
||||
[locale]="data.locale"
|
||||
[value]="assetProfile?.countries[0].name"
|
||||
>Country</gf-value
|
||||
>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #charts>
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="h5" i18n>Sectors</div>
|
||||
<gf-portfolio-proportion-chart
|
||||
[colorScheme]="data.colorScheme"
|
||||
[isInPercent]="true"
|
||||
[keys]="['name']"
|
||||
[maxItems]="10"
|
||||
[positions]="sectors"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="h5" i18n>Countries</div>
|
||||
<gf-portfolio-proportion-chart
|
||||
[colorScheme]="data.colorScheme"
|
||||
[isInPercent]="true"
|
||||
[keys]="['name']"
|
||||
[maxItems]="10"
|
||||
[positions]="countries"
|
||||
/>
|
||||
</div>
|
||||
</ng-template>
|
||||
@if (assetProfile?.countries?.length === 1 &&
|
||||
assetProfile?.sectors?.length === 1 ) {
|
||||
<div *ngIf="assetProfile?.sectors?.length === 1" class="col-6 mb-3">
|
||||
<gf-value
|
||||
i18n
|
||||
size="medium"
|
||||
[locale]="data.locale"
|
||||
[value]="assetProfile?.sectors[0].name"
|
||||
>Sector</gf-value
|
||||
>
|
||||
</div>
|
||||
<div *ngIf="assetProfile?.countries?.length === 1" class="col-6 mb-3">
|
||||
<gf-value
|
||||
i18n
|
||||
size="medium"
|
||||
[locale]="data.locale"
|
||||
[value]="assetProfile?.countries[0].name"
|
||||
>Country</gf-value
|
||||
>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="h5" i18n>Sectors</div>
|
||||
<gf-portfolio-proportion-chart
|
||||
[colorScheme]="data.colorScheme"
|
||||
[isInPercent]="true"
|
||||
[keys]="['name']"
|
||||
[maxItems]="10"
|
||||
[positions]="sectors"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="h5" i18n>Countries</div>
|
||||
<gf-portfolio-proportion-chart
|
||||
[colorScheme]="data.colorScheme"
|
||||
[isInPercent]="true"
|
||||
[keys]="['name']"
|
||||
[maxItems]="10"
|
||||
[positions]="countries"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</ng-container>
|
||||
</div>
|
||||
<div *ngIf="assetProfile?.dataSource === 'MANUAL'" class="mt-3">
|
||||
|
@ -217,7 +217,7 @@ export class HeaderComponent implements OnChanges {
|
||||
this.signOut.next();
|
||||
}
|
||||
|
||||
public openLoginDialog(): void {
|
||||
public openLoginDialog() {
|
||||
const dialogRef = this.dialog.open(LoginWithAccessTokenDialog, {
|
||||
autoFocus: false,
|
||||
data: {
|
||||
|
@ -1,10 +1,8 @@
|
||||
<div
|
||||
class="align-items-center container d-flex flex-column h-100 justify-content-center overview p-0 position-relative"
|
||||
>
|
||||
<div
|
||||
*ngIf="hasPermissionToCreateOrder && historicalDataItems?.length === 0; else isUserActive"
|
||||
class="justify-content-center row w-100"
|
||||
>
|
||||
@if(hasPermissionToCreateOrder && historicalDataItems?.length === 0) {
|
||||
<div class="justify-content-center row w-100">
|
||||
<div class="col introduction">
|
||||
<h4 i18n>Welcome to Ghostfolio</h4>
|
||||
<p i18n>Ready to take control of your personal finances?</p>
|
||||
@ -60,43 +58,43 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #isUserActive>
|
||||
<div class="row w-100">
|
||||
<div class="col p-0">
|
||||
<div class="chart-container mx-auto position-relative">
|
||||
<gf-line-chart
|
||||
class="position-absolute"
|
||||
symbol="Performance"
|
||||
unit="%"
|
||||
[colorScheme]="user?.settings?.colorScheme"
|
||||
[hidden]="historicalDataItems?.length === 0"
|
||||
[historicalDataItems]="historicalDataItems"
|
||||
[isAnimated]="user?.settings?.dateRange === '1d' ? false : true"
|
||||
[locale]="user?.settings?.locale"
|
||||
[ngClass]="{ 'pr-3': deviceType === 'mobile' }"
|
||||
[showGradient]="true"
|
||||
[showLoader]="false"
|
||||
[showXAxis]="false"
|
||||
[showYAxis]="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overview-container row mt-1">
|
||||
<div class="col">
|
||||
<gf-portfolio-performance
|
||||
class="pb-4"
|
||||
[deviceType]="deviceType"
|
||||
[errors]="errors"
|
||||
[isAllTimeHigh]="isAllTimeHigh"
|
||||
[isAllTimeLow]="isAllTimeLow"
|
||||
[isLoading]="isLoadingPerformance"
|
||||
} @else {
|
||||
<div class="row w-100">
|
||||
<div class="col p-0">
|
||||
<div class="chart-container mx-auto position-relative">
|
||||
<gf-line-chart
|
||||
class="position-absolute"
|
||||
symbol="Performance"
|
||||
unit="%"
|
||||
[colorScheme]="user?.settings?.colorScheme"
|
||||
[hidden]="historicalDataItems?.length === 0"
|
||||
[historicalDataItems]="historicalDataItems"
|
||||
[isAnimated]="user?.settings?.dateRange === '1d' ? false : true"
|
||||
[locale]="user?.settings?.locale"
|
||||
[performance]="performance"
|
||||
[showDetails]="showDetails"
|
||||
[unit]="unit"
|
||||
[ngClass]="{ 'pr-3': deviceType === 'mobile' }"
|
||||
[showGradient]="true"
|
||||
[showLoader]="false"
|
||||
[showXAxis]="false"
|
||||
[showYAxis]="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="overview-container row mt-1">
|
||||
<div class="col">
|
||||
<gf-portfolio-performance
|
||||
class="pb-4"
|
||||
[deviceType]="deviceType"
|
||||
[errors]="errors"
|
||||
[isAllTimeHigh]="isAllTimeHigh"
|
||||
[isAllTimeLow]="isAllTimeLow"
|
||||
[isLoading]="isLoadingPerformance"
|
||||
[locale]="user?.settings?.locale"
|
||||
[performance]="performance"
|
||||
[showDetails]="showDetails"
|
||||
[unit]="unit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
@ -38,8 +38,16 @@ import {
|
||||
} from 'chart.js';
|
||||
import 'chartjs-adapter-date-fns';
|
||||
import annotationPlugin from 'chartjs-plugin-annotation';
|
||||
import { addDays, format, isAfter, parseISO, subDays } from 'date-fns';
|
||||
import { last } from 'lodash';
|
||||
import {
|
||||
addDays,
|
||||
format,
|
||||
isAfter,
|
||||
isValid,
|
||||
min,
|
||||
parseISO,
|
||||
subDays
|
||||
} from 'date-fns';
|
||||
import { first, last } from 'lodash';
|
||||
|
||||
@Component({
|
||||
selector: 'gf-investment-chart',
|
||||
@ -143,7 +151,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
const chartData: ChartData<'line'> = {
|
||||
const chartData: ChartData<'bar' | 'line'> = {
|
||||
labels: this.historicalDataItems.map(({ date }) => {
|
||||
return parseDate(date);
|
||||
}),
|
||||
@ -194,17 +202,23 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
};
|
||||
|
||||
if (this.chartCanvas) {
|
||||
let scaleXMin: string;
|
||||
|
||||
if (this.daysInMarket) {
|
||||
const minDate = min([
|
||||
parseDate(first(this.investments)?.date),
|
||||
subDays(new Date().setHours(0, 0, 0, 0), this.daysInMarket)
|
||||
]);
|
||||
|
||||
scaleXMin = isValid(minDate) ? minDate.toISOString() : undefined;
|
||||
}
|
||||
|
||||
if (this.chart) {
|
||||
this.chart.data = chartData;
|
||||
this.chart.options.plugins.tooltip = <unknown>(
|
||||
this.getTooltipPluginConfiguration()
|
||||
);
|
||||
this.chart.options.scales.x.min = this.daysInMarket
|
||||
? subDays(
|
||||
new Date().setHours(0, 0, 0, 0),
|
||||
this.daysInMarket
|
||||
).toISOString()
|
||||
: undefined;
|
||||
this.chart.options.scales.x.min = scaleXMin;
|
||||
|
||||
if (
|
||||
this.savingsRate &&
|
||||
@ -287,9 +301,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
grid: {
|
||||
display: false
|
||||
},
|
||||
min: this.daysInMarket
|
||||
? subDays(new Date(), this.daysInMarket).toISOString()
|
||||
: undefined,
|
||||
min: scaleXMin,
|
||||
suggestedMax: new Date().toISOString(),
|
||||
type: 'time',
|
||||
time: {
|
||||
|
@ -19,9 +19,9 @@ import {
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { Sort, SortDirection } from '@angular/material/sort';
|
||||
import { SortDirection } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { Tag } from '@prisma/client';
|
||||
import { Account, Tag } from '@prisma/client';
|
||||
import { format, isSameMonth, isToday, parseISO } from 'date-fns';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
@ -36,6 +36,7 @@ import { PositionDetailDialogParams } from './interfaces/interfaces';
|
||||
styleUrls: ['./position-detail-dialog.component.scss']
|
||||
})
|
||||
export class PositionDetailDialog implements OnDestroy, OnInit {
|
||||
public accounts: Account[];
|
||||
public activities: OrderWithAccount[];
|
||||
public assetClass: string;
|
||||
public assetSubClass: string;
|
||||
@ -83,7 +84,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
||||
private userService: UserService
|
||||
) {}
|
||||
|
||||
public ngOnInit(): void {
|
||||
public ngOnInit() {
|
||||
this.dataService
|
||||
.fetchPositionDetail({
|
||||
dataSource: this.data.dataSource,
|
||||
@ -92,6 +93,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(
|
||||
({
|
||||
accounts,
|
||||
averagePrice,
|
||||
dataProviderInfo,
|
||||
dividendInBaseCurrency,
|
||||
@ -113,6 +115,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
||||
transactionCount,
|
||||
value
|
||||
}) => {
|
||||
this.accounts = accounts;
|
||||
this.activities = orders;
|
||||
this.averagePrice = averagePrice;
|
||||
this.benchmarkDataItems = [];
|
||||
@ -264,7 +267,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
public onClose(): void {
|
||||
public onClose() {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
|
@ -186,57 +186,52 @@
|
||||
<ng-container
|
||||
*ngIf="SymbolProfile?.countries?.length > 0 || SymbolProfile?.sectors?.length > 0"
|
||||
>
|
||||
<ng-container
|
||||
*ngIf="SymbolProfile?.countries?.length === 1 && SymbolProfile?.sectors?.length === 1; else charts"
|
||||
>
|
||||
<div *ngIf="SymbolProfile?.sectors?.length === 1" class="col-6 mb-3">
|
||||
<gf-value
|
||||
i18n
|
||||
size="medium"
|
||||
[locale]="data.locale"
|
||||
[value]="SymbolProfile.sectors[0].name"
|
||||
>Sector</gf-value
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
*ngIf="SymbolProfile?.countries?.length === 1"
|
||||
class="col-6 mb-3"
|
||||
@if(SymbolProfile?.countries?.length === 1 &&
|
||||
SymbolProfile?.sectors?.length === 1) {
|
||||
<div *ngIf="SymbolProfile?.sectors?.length === 1" class="col-6 mb-3">
|
||||
<gf-value
|
||||
i18n
|
||||
size="medium"
|
||||
[locale]="data.locale"
|
||||
[value]="SymbolProfile.sectors[0].name"
|
||||
>Sector</gf-value
|
||||
>
|
||||
<gf-value
|
||||
i18n
|
||||
size="medium"
|
||||
[locale]="data.locale"
|
||||
[value]="SymbolProfile.countries[0].name"
|
||||
>Country</gf-value
|
||||
>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #charts>
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="h5" i18n>Sectors</div>
|
||||
<gf-portfolio-proportion-chart
|
||||
[baseCurrency]="data.baseCurrency"
|
||||
[colorScheme]="data.colorScheme"
|
||||
[isInPercent]="true"
|
||||
[keys]="['name']"
|
||||
[locale]="data.locale"
|
||||
[maxItems]="10"
|
||||
[positions]="sectors"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="h5" i18n>Countries</div>
|
||||
<gf-portfolio-proportion-chart
|
||||
[baseCurrency]="data.baseCurrency"
|
||||
[colorScheme]="data.colorScheme"
|
||||
[isInPercent]="true"
|
||||
[keys]="['name']"
|
||||
[locale]="data.locale"
|
||||
[maxItems]="10"
|
||||
[positions]="countries"
|
||||
/>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div *ngIf="SymbolProfile?.countries?.length === 1" class="col-6 mb-3">
|
||||
<gf-value
|
||||
i18n
|
||||
size="medium"
|
||||
[locale]="data.locale"
|
||||
[value]="SymbolProfile.countries[0].name"
|
||||
>Country</gf-value
|
||||
>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="h5" i18n>Sectors</div>
|
||||
<gf-portfolio-proportion-chart
|
||||
[baseCurrency]="data.baseCurrency"
|
||||
[colorScheme]="data.colorScheme"
|
||||
[isInPercent]="true"
|
||||
[keys]="['name']"
|
||||
[locale]="data.locale"
|
||||
[maxItems]="10"
|
||||
[positions]="sectors"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="h5" i18n>Countries</div>
|
||||
<gf-portfolio-proportion-chart
|
||||
[baseCurrency]="data.baseCurrency"
|
||||
[colorScheme]="data.colorScheme"
|
||||
[isInPercent]="true"
|
||||
[keys]="['name']"
|
||||
[locale]="data.locale"
|
||||
[maxItems]="10"
|
||||
[positions]="countries"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</ng-container>
|
||||
<div *ngIf="dataProviderInfo" class="col-md-12 mb-3 text-center">
|
||||
<hr />
|
||||
@ -246,9 +241,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" [ngClass]="{ 'd-none': !activities?.length }">
|
||||
<div class="col mb-3">
|
||||
<div class="h5 mb-0" i18n>Activities</div>
|
||||
<mat-tab-group
|
||||
animationDuration="0"
|
||||
class="mb-3"
|
||||
[mat-stretch-tabs]="false"
|
||||
[ngClass]="{ 'd-none': !activities?.length }"
|
||||
>
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<ion-icon name="swap-vertical-outline" />
|
||||
<div class="d-none d-sm-block ml-2" i18n>Activities</div>
|
||||
</ng-template>
|
||||
<gf-activities-table
|
||||
[baseCurrency]="data.baseCurrency"
|
||||
[dataSource]="dataSource"
|
||||
@ -266,8 +269,26 @@
|
||||
[totalItems]="totalItems"
|
||||
(export)="onExport()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<ion-icon name="wallet-outline" />
|
||||
<div class="d-none d-sm-block ml-2" i18n>Accounts</div>
|
||||
</ng-template>
|
||||
<gf-accounts-table
|
||||
[accounts]="accounts"
|
||||
[baseCurrency]="user?.settings?.baseCurrency"
|
||||
[deviceType]="data.deviceType"
|
||||
[hasPermissionToOpenDetails]="false"
|
||||
[locale]="user?.settings?.locale"
|
||||
[showBalance]="false"
|
||||
[showFooter]="false"
|
||||
[showTransactions]="false"
|
||||
[showValue]="false"
|
||||
[showValueInBaseCurrency]="false"
|
||||
/>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
|
||||
<div *ngIf="tags?.length > 0" class="row">
|
||||
<div class="col">
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { GfAccountsTableModule } from '@ghostfolio/client/components/accounts-table/accounts-table.module';
|
||||
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';
|
||||
@ -11,6 +12,7 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
|
||||
import { PositionDetailDialog } from './position-detail-dialog.component';
|
||||
@ -19,6 +21,7 @@ import { PositionDetailDialog } from './position-detail-dialog.component';
|
||||
declarations: [PositionDetailDialog],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfAccountsTableModule,
|
||||
GfActivitiesTableModule,
|
||||
GfDataProviderCreditsModule,
|
||||
GfDialogFooterModule,
|
||||
@ -29,6 +32,7 @@ import { PositionDetailDialog } from './position-detail-dialog.component';
|
||||
MatButtonModule,
|
||||
MatChipsModule,
|
||||
MatDialogModule,
|
||||
MatTabsModule,
|
||||
NgxSkeletonLoaderModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
|
@ -100,7 +100,7 @@ export class UserAccountAccessComponent implements OnDestroy, OnInit {
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
private openCreateAccessDialog(): void {
|
||||
private openCreateAccessDialog() {
|
||||
const dialogRef = this.dialog.open(CreateOrUpdateAccessDialog, {
|
||||
data: {
|
||||
access: {
|
||||
|
@ -3,7 +3,6 @@ import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import * as path from 'path';
|
||||
|
||||
import { AboutPageComponent } from './about-page.component';
|
||||
|
||||
|
@ -8,7 +8,7 @@ import { AboutPageComponent } from './about-page.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AboutPageComponent],
|
||||
imports: [CommonModule, MatTabsModule, AboutPageRoutingModule, RouterModule],
|
||||
imports: [AboutPageRoutingModule, CommonModule, MatTabsModule, RouterModule],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class AboutPageModule {}
|
||||
|
@ -13,7 +13,6 @@ import { takeUntil } from 'rxjs/operators';
|
||||
templateUrl: './about-overview-page.html'
|
||||
})
|
||||
export class AboutOverviewPageComponent implements OnDestroy, OnInit {
|
||||
public hasPermissionForBlog: boolean;
|
||||
public hasPermissionForStatistics: boolean;
|
||||
public hasPermissionForSubscription: boolean;
|
||||
public isLoggedIn: boolean;
|
||||
@ -30,11 +29,6 @@ export class AboutOverviewPageComponent implements OnDestroy, OnInit {
|
||||
) {
|
||||
const { globalPermissions } = this.dataService.fetchInfo();
|
||||
|
||||
this.hasPermissionForBlog = hasPermission(
|
||||
globalPermissions,
|
||||
permissions.enableBlog
|
||||
);
|
||||
|
||||
this.hasPermissionForStatistics = hasPermission(
|
||||
globalPermissions,
|
||||
permissions.enableStatistics
|
||||
|
@ -133,16 +133,21 @@
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div *ngIf="hasPermissionForSubscription" class="col-md-6 col-xs-12 my-2">
|
||||
<div
|
||||
class="col-md-6 col-xs-12 my-2"
|
||||
[ngClass]="{ 'offset-md-3': hasPermissionForSubscription === false }"
|
||||
>
|
||||
<a
|
||||
class="py-4 w-100"
|
||||
color="primary"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[routerLink]="routerLinkFaq"
|
||||
>Frequently Asked Questions (FAQ)</a
|
||||
>
|
||||
</div>
|
||||
<div *ngIf="hasPermissionForBlog" class="col-md-6 col-xs-12 my-2">
|
||||
@if (hasPermissionForSubscription) {
|
||||
<div class="col-md-6 col-xs-12 my-2">
|
||||
<a
|
||||
class="py-4 w-100"
|
||||
color="primary"
|
||||
@ -151,5 +156,6 @@
|
||||
>Blog</a
|
||||
>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -169,7 +169,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
||||
isExcluded,
|
||||
name,
|
||||
platformId
|
||||
}: AccountModel): void {
|
||||
}: AccountModel) {
|
||||
const dialogRef = this.dialog.open(CreateOrUpdateAccountDialog, {
|
||||
data: {
|
||||
account: {
|
||||
@ -237,7 +237,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
private openCreateAccountDialog(): void {
|
||||
private openCreateAccountDialog() {
|
||||
const dialogRef = this.dialog.open(CreateOrUpdateAccountDialog, {
|
||||
data: {
|
||||
account: {
|
||||
@ -279,7 +279,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
private openTransferBalanceDialog(): void {
|
||||
private openTransferBalanceDialog() {
|
||||
const dialogRef = this.dialog.open(TransferBalanceDialog, {
|
||||
data: {
|
||||
accounts: this.accounts
|
||||
|
@ -8,6 +8,27 @@ import { FaqPageComponent } from './faq-page.component';
|
||||
const routes: Routes = [
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () =>
|
||||
import('./overview/faq-overview-page.module').then(
|
||||
(m) => m.FaqOverviewPageModule
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'saas',
|
||||
loadChildren: () =>
|
||||
import('./saas/saas-page.module').then((m) => m.SaasPageModule)
|
||||
},
|
||||
{
|
||||
path: 'self-hosting',
|
||||
loadChildren: () =>
|
||||
import('./self-hosting/self-hosting-page.module').then(
|
||||
(m) => m.SelfHostingPageModule
|
||||
)
|
||||
}
|
||||
],
|
||||
component: FaqPageComponent,
|
||||
path: '',
|
||||
title: $localize`Frequently Asked Questions (FAQ)`
|
||||
|
@ -1,39 +1,57 @@
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { User } from '@ghostfolio/common/interfaces';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { TabConfiguration, User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
|
||||
import { ChangeDetectorRef, Component, OnDestroy } from '@angular/core';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
host: { class: 'page has-tabs' },
|
||||
selector: 'gf-faq-page',
|
||||
styleUrls: ['./faq-page.scss'],
|
||||
templateUrl: './faq-page.html'
|
||||
})
|
||||
export class FaqPageComponent implements OnDestroy {
|
||||
public routerLinkFeatures = ['/' + $localize`features`];
|
||||
public routerLinkMarkets = ['/' + $localize`markets`];
|
||||
public routerLinkPricing = ['/' + $localize`pricing`];
|
||||
public routerLinkRegister = ['/' + $localize`register`];
|
||||
public user: User;
|
||||
export class FaqPageComponent implements OnDestroy, OnInit {
|
||||
public deviceType: string;
|
||||
public hasPermissionForSubscription: boolean;
|
||||
public tabs: TabConfiguration[] = [];
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor(
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private userService: UserService
|
||||
) {}
|
||||
private dataService: DataService,
|
||||
private deviceService: DeviceDetectorService
|
||||
) {
|
||||
const { globalPermissions } = this.dataService.fetchInfo();
|
||||
|
||||
this.hasPermissionForSubscription = hasPermission(
|
||||
globalPermissions,
|
||||
permissions.enableSubscription
|
||||
);
|
||||
|
||||
this.tabs = [
|
||||
{
|
||||
iconName: 'reader-outline',
|
||||
label: $localize`General`,
|
||||
path: ['/' + $localize`faq`]
|
||||
},
|
||||
{
|
||||
iconName: 'cloudy-outline',
|
||||
label: $localize`Cloud` + ' (SaaS)',
|
||||
path: ['/' + $localize`faq`, 'saas'],
|
||||
showCondition: this.hasPermissionForSubscription
|
||||
},
|
||||
{
|
||||
iconName: 'server-outline',
|
||||
label: $localize`Self-Hosting`,
|
||||
path: ['/' + $localize`faq`, $localize`self-hosting`]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
public ngOnInit() {
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((state) => {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
|
@ -1,291 +1,29 @@
|
||||
<div class="container">
|
||||
<div class="mb-5 row">
|
||||
<div class="col">
|
||||
<h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>
|
||||
Frequently Asked Questions (FAQ)
|
||||
</h1>
|
||||
<p>
|
||||
Find quick answers to commonly asked questions about Ghostfolio in our
|
||||
Frequently Asked Questions (FAQ) section. Discover what Ghostfolio is,
|
||||
explore its features, and learn about our privacy practices. Get all the
|
||||
information you need in one place.
|
||||
</p>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title>What is Ghostfolio?</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
Ghostfolio is a lightweight, open source wealth management application
|
||||
for individuals to keep track of their net worth. The software
|
||||
empowers you to make solid, data-driven investment decisions.
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title
|
||||
>What assets can I track with Ghostfolio?</mat-card-title
|
||||
>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
With Ghostfolio, you can keep track of various assets like stocks,
|
||||
ETFs, bonds, cryptocurrencies and commodities.
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title
|
||||
>What else is included in Ghostfolio?</mat-card-title
|
||||
></mat-card-header
|
||||
>
|
||||
<mat-card-content>
|
||||
Please find a feature overview to manage your wealth
|
||||
<a [routerLink]="routerLinkFeatures">here</a>.
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title>How do I start?</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
You can sign up via the “<a [routerLink]="routerLinkRegister"
|
||||
>Get Started</a
|
||||
>” button at the top of the page. You have multiple options to join
|
||||
Ghostfolio: Create an account with a security token or
|
||||
<i>Google Sign</i>. We will guide you to set up your portfolio.
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title
|
||||
>Will you spam me with emails once I sign up?</mat-card-title
|
||||
></mat-card-header
|
||||
>
|
||||
<mat-card-content>
|
||||
No, we do not even collect your email address, so you will not receive
|
||||
any spam emails from us.
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title
|
||||
>Can I use Ghostfolio anonymously?</mat-card-title
|
||||
></mat-card-header
|
||||
>
|
||||
<mat-card-content>
|
||||
Yes, the authentication system via security token enables you to sign
|
||||
in securely and anonymously to Ghostfolio. There is no need for an
|
||||
e-mail address, phone number, or a username.
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title
|
||||
>How can Ghostfolio be free?</mat-card-title
|
||||
></mat-card-header
|
||||
>
|
||||
<mat-card-content
|
||||
>This project is driven by the efforts of contributors from around the
|
||||
world. The
|
||||
<a href="https://github.com/ghostfolio/ghostfolio">source code</a> is
|
||||
fully available as open source software (OSS). Thanks to our generous
|
||||
<a [routerLink]="routerLinkPricing">Ghostfolio Premium</a> users and
|
||||
<a href="https://www.buymeacoffee.com/ghostfolio">sponsors</a> we have
|
||||
the ability to run a free, limited plan for novice
|
||||
investors.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Is it really free?</mat-card-title></mat-card-header
|
||||
>
|
||||
<mat-card-content
|
||||
>Yes, it is! Our
|
||||
<a [routerLink]="routerLinkPricing">pricing page</a> details
|
||||
everything you get for free.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title
|
||||
>Do you monetize or sell my financial data?</mat-card-title
|
||||
></mat-card-header
|
||||
>
|
||||
<mat-card-content
|
||||
>No, we value your privacy. We do not sell or share your financial
|
||||
data with any third parties.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title
|
||||
>What is your business model?</mat-card-title
|
||||
></mat-card-header
|
||||
>
|
||||
<mat-card-content
|
||||
>By offering
|
||||
<a [routerLink]="routerLinkPricing">Ghostfolio Premium</a>, a
|
||||
subscription plan with a managed hosting service and enhanced
|
||||
features, we fund our business while providing added value to our
|
||||
users.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title
|
||||
>What is Ghostfolio Premium?</mat-card-title
|
||||
></mat-card-header
|
||||
>
|
||||
<mat-card-content
|
||||
><a [routerLink]="routerLinkPricing">Ghostfolio Premium</a> is a fully
|
||||
managed Ghostfolio cloud offering for ambitious investors. Revenue is
|
||||
used to cover the costs of the hosting infrastructure and to fund
|
||||
ongoing development. It is the Open Source code base with some extras
|
||||
like the <a [routerLink]="routerLinkMarkets">markets overview</a> and
|
||||
a professional data provider.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title
|
||||
>Can I start with a trial version?</mat-card-title
|
||||
></mat-card-header
|
||||
>
|
||||
<mat-card-content
|
||||
>Yes, you can try
|
||||
<a [routerLink]="routerLinkPricing">Ghostfolio Premium</a> by signing
|
||||
up for Ghostfolio and applying for a trial (see “My Ghostfolio”). It
|
||||
is easy, free and there is no commitment. You can stop using it at any
|
||||
time.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title
|
||||
>How can I get a student discount for Ghostfolio
|
||||
Premium?</mat-card-title
|
||||
>
|
||||
</mat-card-header>
|
||||
<mat-card-content
|
||||
>Request your student discount
|
||||
<a href="mailto:hi@ghostfol.io?Subject=Student Discount">here</a> with
|
||||
your university e-mail address.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title
|
||||
>Does the Ghostfolio Premium subscription renew
|
||||
automatically?</mat-card-title
|
||||
>
|
||||
</mat-card-header>
|
||||
<mat-card-content
|
||||
>No, <a [routerLink]="routerLinkPricing">Ghostfolio Premium</a> does
|
||||
not include auto-renewal. Upon expiration, you can choose whether to
|
||||
start a new subscription.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Which devices are supported?</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content
|
||||
>Ghostfolio works in every modern web browser on smartphones, tablets
|
||||
and desktop computers. For <i>Android</i> users, there is a dedicated
|
||||
Ghostfolio app available in the
|
||||
<a
|
||||
href="https://play.google.com/store/apps/details?id=ch.dotsilver.ghostfolio.twa"
|
||||
>Google Play Store</a
|
||||
>.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card
|
||||
*ngIf="user?.subscription?.type === 'Premium'"
|
||||
appearance="outlined"
|
||||
class="mb-3"
|
||||
>
|
||||
<mat-card-header>
|
||||
<mat-card-title
|
||||
>I cannot find my broker in the list of platforms. What can I
|
||||
do?</mat-card-title
|
||||
>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
Please send an e-mail with the web address of your broker to
|
||||
<a href="mailto:hi@ghostfol.io">hi@ghostfol.io</a> and we are
|
||||
happy to add it.
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title
|
||||
>Ghostfolio sounds cool, how can I get involved?</mat-card-title
|
||||
>
|
||||
</mat-card-header>
|
||||
<mat-card-content
|
||||
>Any support for Ghostfolio is welcome. Be it with a
|
||||
<a [routerLink]="routerLinkPricing">Ghostfolio Premium</a>
|
||||
subscription to finance the hosting infrastructure, a positive rating
|
||||
in the
|
||||
<a
|
||||
href="https://play.google.com/store/apps/details?id=ch.dotsilver.ghostfolio.twa"
|
||||
>Google Play Store</a
|
||||
>, a star on
|
||||
<a href="https://github.com/ghostfolio/ghostfolio">GitHub</a>,
|
||||
feedback, bug reports, feature requests and of course contributions!
|
||||
You can reach us via Ghostfolio
|
||||
<a
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
title="Join the Ghostfolio Slack community"
|
||||
>Slack</a
|
||||
>
|
||||
community,
|
||||
<a
|
||||
href="https://twitter.com/ghostfolio_"
|
||||
title="Post to Ghostfolio on X (formerly Twitter)"
|
||||
>@ghostfolio_</a
|
||||
><ng-container *ngIf="user?.subscription?.type === 'Premium'"
|
||||
>,
|
||||
<a href="mailto:hi@ghostfol.io" title="Send an e-mail"
|
||||
>hi@ghostfol.io</a
|
||||
></ng-container
|
||||
>
|
||||
or
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio"
|
||||
title="Find Ghostfolio on GitHub"
|
||||
>GitHub</a
|
||||
>.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Got any other questions?</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content
|
||||
>Please join the Ghostfolio
|
||||
<a
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
title="Join the Ghostfolio Slack community"
|
||||
>Slack </a
|
||||
>community, post to
|
||||
<a
|
||||
href="https://twitter.com/ghostfolio_"
|
||||
title="Post to Ghostfolio on X (formerly Twitter)"
|
||||
>@ghostfolio_</a
|
||||
><ng-container *ngIf="user?.subscription?.type === 'Premium'"
|
||||
>, send an e-mail to
|
||||
<a href="mailto:hi@ghostfol.io" title="Send an e-mail"
|
||||
>hi@ghostfol.io</a
|
||||
></ng-container
|
||||
>
|
||||
or start a discussion at
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio"
|
||||
title="Find Ghostfolio on GitHub"
|
||||
>GitHub</a
|
||||
>.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<mat-tab-nav-panel #tabPanel class="flex-grow-1 overflow-auto">
|
||||
<router-outlet></router-outlet>
|
||||
</mat-tab-nav-panel>
|
||||
|
||||
<nav
|
||||
mat-align-tabs="center"
|
||||
mat-tab-nav-bar
|
||||
[disablePagination]="true"
|
||||
[tabPanel]="tabPanel"
|
||||
>
|
||||
<ng-container *ngFor="let tab of tabs">
|
||||
<a
|
||||
#rla="routerLinkActive"
|
||||
*ngIf="tab.showCondition !== false"
|
||||
class="no-min-width px-3"
|
||||
mat-tab-link
|
||||
routerLinkActive
|
||||
[active]="rla.isActive"
|
||||
[routerLink]="tab.path"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
>
|
||||
<ion-icon
|
||||
[name]="tab.iconName"
|
||||
[size]="deviceType === 'mobile' ? 'large': 'small'"
|
||||
/>
|
||||
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
|
||||
</a>
|
||||
</ng-container>
|
||||
</nav>
|
||||
|
@ -1,13 +1,14 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { FaqPageRoutingModule } from './faq-page-routing.module';
|
||||
import { FaqPageComponent } from './faq-page.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [FaqPageComponent],
|
||||
imports: [CommonModule, FaqPageRoutingModule, MatCardModule],
|
||||
imports: [CommonModule, FaqPageRoutingModule, MatTabsModule, RouterModule],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class FaqPageModule {}
|
||||
|
@ -1,12 +1,7 @@
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
a {
|
||||
color: rgba(var(--palette-primary-500), 1);
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
color: rgba(var(--palette-primary-300), 1);
|
||||
}
|
||||
}
|
||||
color: rgb(var(--dark-primary-text));
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
color: rgb(var(--light-primary-text));
|
||||
}
|
||||
|
@ -0,0 +1,21 @@
|
||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { FaqOverviewPageComponent } from './faq-overview-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
component: FaqOverviewPageComponent,
|
||||
path: '',
|
||||
title: $localize`Frequently Asked Questions (FAQ)`
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class FaqOverviewPageRoutingModule {}
|
@ -0,0 +1,40 @@
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { User } from '@ghostfolio/common/interfaces';
|
||||
|
||||
import { ChangeDetectorRef, Component, OnDestroy } from '@angular/core';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
selector: 'gf-faq-overview-page',
|
||||
styleUrls: ['./faq-overview-page.scss'],
|
||||
templateUrl: './faq-overview-page.html'
|
||||
})
|
||||
export class FaqOverviewPageComponent implements OnDestroy {
|
||||
public routerLinkFeatures = ['/' + $localize`features`];
|
||||
public user: User;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor(
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private userService: UserService
|
||||
) {}
|
||||
|
||||
public ngOnInit() {
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((state) => {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
}
|
172
apps/client/src/app/pages/faq/overview/faq-overview-page.html
Normal file
172
apps/client/src/app/pages/faq/overview/faq-overview-page.html
Normal file
@ -0,0 +1,172 @@
|
||||
<div class="container">
|
||||
<div class="mb-5 row">
|
||||
<div class="col">
|
||||
<h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>
|
||||
Frequently Asked Questions (FAQ)
|
||||
</h1>
|
||||
<p>
|
||||
Find quick answers to commonly asked questions about Ghostfolio in our
|
||||
Frequently Asked Questions (FAQ) section. Discover what Ghostfolio is,
|
||||
explore its features, and learn about our privacy practices. Get all the
|
||||
information you need in one place.
|
||||
</p>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title>What is Ghostfolio?</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
Ghostfolio is a lightweight, open source wealth management application
|
||||
for individuals to keep track of their net worth. The software
|
||||
empowers you to make solid, data-driven investment decisions.
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title
|
||||
>What assets can I track with Ghostfolio?</mat-card-title
|
||||
>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
With Ghostfolio, you can keep track of various assets like stocks,
|
||||
ETFs, bonds, cryptocurrencies and commodities.
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title
|
||||
>What else is included in Ghostfolio?</mat-card-title
|
||||
></mat-card-header
|
||||
>
|
||||
<mat-card-content>
|
||||
Please find a feature overview to manage your wealth
|
||||
<a [routerLink]="routerLinkFeatures">here</a>.
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title
|
||||
>Can I use Ghostfolio anonymously?</mat-card-title
|
||||
></mat-card-header
|
||||
>
|
||||
<mat-card-content>
|
||||
Yes, the authentication system via security token enables you to sign
|
||||
in securely and anonymously to Ghostfolio. There is no need for an
|
||||
e-mail address, phone number, or a username.
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title
|
||||
>How can Ghostfolio be free?</mat-card-title
|
||||
></mat-card-header
|
||||
>
|
||||
<mat-card-content
|
||||
>This project is driven by the efforts of contributors from around the
|
||||
world. The
|
||||
<a href="https://github.com/ghostfolio/ghostfolio">source code</a> is
|
||||
fully available as open source software (OSS). Thanks to our generous
|
||||
<a href="https://ghostfol.io/en/pricing">Ghostfolio Premium</a> users
|
||||
and <a href="https://www.buymeacoffee.com/ghostfolio">sponsors</a> we
|
||||
have the ability to run a free, limited plan for novice
|
||||
investors.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title
|
||||
>Do you monetize or sell my financial data?</mat-card-title
|
||||
></mat-card-header
|
||||
>
|
||||
<mat-card-content
|
||||
>No, we value your privacy. We do not sell or share your financial
|
||||
data with any third parties.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title
|
||||
>What is your business model?</mat-card-title
|
||||
></mat-card-header
|
||||
>
|
||||
<mat-card-content
|
||||
>By offering
|
||||
<a href="https://ghostfol.io/en/pricing">Ghostfolio Premium</a>, a
|
||||
subscription plan with a managed hosting service and enhanced
|
||||
features, we fund our business while providing added value to our
|
||||
users.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title
|
||||
>Ghostfolio sounds cool, how can I get involved?</mat-card-title
|
||||
>
|
||||
</mat-card-header>
|
||||
<mat-card-content
|
||||
>Any support for Ghostfolio is welcome. Be it with a
|
||||
<a href="https://ghostfol.io/en/pricing">Ghostfolio Premium</a>
|
||||
subscription to finance the hosting infrastructure, a positive rating
|
||||
in the
|
||||
<a
|
||||
href="https://play.google.com/store/apps/details?id=ch.dotsilver.ghostfolio.twa"
|
||||
>Google Play Store</a
|
||||
>, a star on
|
||||
<a href="https://github.com/ghostfolio/ghostfolio">GitHub</a>,
|
||||
feedback, bug reports, feature requests and of course contributions!
|
||||
You can reach us via Ghostfolio
|
||||
<a
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
title="Join the Ghostfolio Slack community"
|
||||
>Slack</a
|
||||
>
|
||||
community,
|
||||
<a
|
||||
href="https://twitter.com/ghostfolio_"
|
||||
title="Post to Ghostfolio on X (formerly Twitter)"
|
||||
>@ghostfolio_</a
|
||||
><ng-container *ngIf="user?.subscription?.type === 'Premium'"
|
||||
>,
|
||||
<a href="mailto:hi@ghostfol.io" title="Send an e-mail"
|
||||
>hi@ghostfol.io</a
|
||||
></ng-container
|
||||
>
|
||||
or
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio"
|
||||
title="Find Ghostfolio on GitHub"
|
||||
>GitHub</a
|
||||
>.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Got any other questions?</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content
|
||||
>Please join the Ghostfolio
|
||||
<a
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
title="Join the Ghostfolio Slack community"
|
||||
>Slack </a
|
||||
>community, post to
|
||||
<a
|
||||
href="https://twitter.com/ghostfolio_"
|
||||
title="Post to Ghostfolio on X (formerly Twitter)"
|
||||
>@ghostfolio_</a
|
||||
><ng-container *ngIf="user?.subscription?.type === 'Premium'"
|
||||
>, send an e-mail to
|
||||
<a href="mailto:hi@ghostfol.io" title="Send an e-mail"
|
||||
>hi@ghostfol.io</a
|
||||
></ng-container
|
||||
>
|
||||
or start a discussion at
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio"
|
||||
title="Find Ghostfolio on GitHub"
|
||||
>GitHub</a
|
||||
>.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,13 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
|
||||
import { FaqOverviewPageRoutingModule } from './faq-overview-page-routing.module';
|
||||
import { FaqOverviewPageComponent } from './faq-overview-page.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [FaqOverviewPageComponent],
|
||||
imports: [CommonModule, FaqOverviewPageRoutingModule, MatCardModule],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class FaqOverviewPageModule {}
|
@ -0,0 +1,12 @@
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
a {
|
||||
color: rgba(var(--palette-primary-500), 1);
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
color: rgba(var(--palette-primary-300), 1);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { SaasPageComponent } from './saas-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
component: SaasPageComponent,
|
||||
path: '',
|
||||
title: $localize`Cloud` + ' (SaaS) – ' + $localize`FAQ`
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class SaasPageRoutingModule {}
|
42
apps/client/src/app/pages/faq/saas/saas-page.component.ts
Normal file
42
apps/client/src/app/pages/faq/saas/saas-page.component.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { User } from '@ghostfolio/common/interfaces';
|
||||
|
||||
import { ChangeDetectorRef, Component, OnDestroy } from '@angular/core';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
selector: 'gf-saas-page',
|
||||
styleUrls: ['./saas-page.scss'],
|
||||
templateUrl: './saas-page.html'
|
||||
})
|
||||
export class SaasPageComponent implements OnDestroy {
|
||||
public routerLinkMarkets = ['/' + $localize`markets`];
|
||||
public routerLinkPricing = ['/' + $localize`pricing`];
|
||||
public routerLinkRegister = ['/' + $localize`register`];
|
||||
public user: User;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor(
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private userService: UserService
|
||||
) {}
|
||||
|
||||
public ngOnInit() {
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((state) => {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
}
|
162
apps/client/src/app/pages/faq/saas/saas-page.html
Normal file
162
apps/client/src/app/pages/faq/saas/saas-page.html
Normal file
@ -0,0 +1,162 @@
|
||||
<div class="container">
|
||||
<div class="mb-5 row">
|
||||
<div class="col">
|
||||
<h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>
|
||||
Frequently Asked Questions (FAQ)
|
||||
</h1>
|
||||
<p>
|
||||
Find quick answers to commonly asked questions about the fully managed
|
||||
Ghostfolio cloud offering in our Frequently Asked Questions (FAQ)
|
||||
section.
|
||||
</p>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title>How do I start?</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
You can sign up via the “<a [routerLink]="routerLinkRegister"
|
||||
>Get Started</a
|
||||
>” button at the top of the page. You have multiple options to join
|
||||
Ghostfolio: Create an account with a security token or
|
||||
<i>Google Sign</i>. We will guide you to set up your portfolio.
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title
|
||||
>Will you spam me with emails once I sign up?</mat-card-title
|
||||
></mat-card-header
|
||||
>
|
||||
<mat-card-content>
|
||||
No, we do not even collect your email address, so you will not receive
|
||||
any spam emails from us.
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Is it really free?</mat-card-title></mat-card-header
|
||||
>
|
||||
<mat-card-content
|
||||
>Yes, it is! Our
|
||||
<a [routerLink]="routerLinkPricing">pricing page</a> details
|
||||
everything you get for free.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title
|
||||
>What is Ghostfolio Premium?</mat-card-title
|
||||
></mat-card-header
|
||||
>
|
||||
<mat-card-content
|
||||
><a [routerLink]="routerLinkPricing">Ghostfolio Premium</a> is a fully
|
||||
managed Ghostfolio cloud offering for ambitious investors. Revenue is
|
||||
used to cover the costs of the hosting infrastructure and to fund
|
||||
ongoing development. It is the Open Source code base with some extras
|
||||
like the <a [routerLink]="routerLinkMarkets">markets overview</a> and
|
||||
a professional data provider.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title
|
||||
>Can I start with a trial version?</mat-card-title
|
||||
></mat-card-header
|
||||
>
|
||||
<mat-card-content
|
||||
>Yes, you can try
|
||||
<a [routerLink]="routerLinkPricing">Ghostfolio Premium</a> by signing
|
||||
up for Ghostfolio and applying for a trial (see “My Ghostfolio”). It
|
||||
is easy, free and there is no commitment. You can stop using it at any
|
||||
time.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title
|
||||
>How can I get a student discount for Ghostfolio
|
||||
Premium?</mat-card-title
|
||||
>
|
||||
</mat-card-header>
|
||||
<mat-card-content
|
||||
>Request your student discount
|
||||
<a href="mailto:hi@ghostfol.io?Subject=Student Discount">here</a> with
|
||||
your university e-mail address.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title
|
||||
>Does the Ghostfolio Premium subscription renew
|
||||
automatically?</mat-card-title
|
||||
>
|
||||
</mat-card-header>
|
||||
<mat-card-content
|
||||
>No, <a [routerLink]="routerLinkPricing">Ghostfolio Premium</a> does
|
||||
not include auto-renewal. Upon expiration, you can choose whether to
|
||||
start a new subscription.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card
|
||||
*ngIf="user?.subscription?.type === 'Premium'"
|
||||
appearance="outlined"
|
||||
class="mb-3"
|
||||
>
|
||||
<mat-card-header>
|
||||
<mat-card-title
|
||||
>I cannot find my broker in the list of platforms. What can I
|
||||
do?</mat-card-title
|
||||
>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
Please send an e-mail with the web address of your broker to
|
||||
<a href="mailto:hi@ghostfol.io">hi@ghostfol.io</a> and we are
|
||||
happy to add it.
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Which devices are supported?</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content
|
||||
>Ghostfolio works in every modern web browser on smartphones, tablets
|
||||
and desktop computers. For <i>Android</i> users of the managed cloud
|
||||
offering, there is a dedicated Ghostfolio app available in the
|
||||
<a
|
||||
href="https://play.google.com/store/apps/details?id=ch.dotsilver.ghostfolio.twa"
|
||||
>Google Play Store</a
|
||||
>.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Got any other questions?</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content
|
||||
>Please join the Ghostfolio
|
||||
<a
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
title="Join the Ghostfolio Slack community"
|
||||
>Slack </a
|
||||
>community, post to
|
||||
<a
|
||||
href="https://twitter.com/ghostfolio_"
|
||||
title="Post to Ghostfolio on X (formerly Twitter)"
|
||||
>@ghostfolio_</a
|
||||
><ng-container *ngIf="user?.subscription?.type === 'Premium'"
|
||||
>, send an e-mail to
|
||||
<a href="mailto:hi@ghostfol.io" title="Send an e-mail"
|
||||
>hi@ghostfol.io</a
|
||||
></ng-container
|
||||
>
|
||||
or start a discussion at
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio"
|
||||
title="Find Ghostfolio on GitHub"
|
||||
>GitHub</a
|
||||
>.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
13
apps/client/src/app/pages/faq/saas/saas-page.module.ts
Normal file
13
apps/client/src/app/pages/faq/saas/saas-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 { SaasPageRoutingModule } from './saas-page-routing.module';
|
||||
import { SaasPageComponent } from './saas-page.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [SaasPageComponent],
|
||||
imports: [CommonModule, MatCardModule, SaasPageRoutingModule],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class SaasPageModule {}
|
12
apps/client/src/app/pages/faq/saas/saas-page.scss
Normal file
12
apps/client/src/app/pages/faq/saas/saas-page.scss
Normal file
@ -0,0 +1,12 @@
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
a {
|
||||
color: rgba(var(--palette-primary-500), 1);
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
color: rgba(var(--palette-primary-300), 1);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { SelfHostingPageComponent } from './self-hosting-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
component: SelfHostingPageComponent,
|
||||
path: '',
|
||||
title: $localize`Self-Hosting` + ' – ' + $localize`FAQ`
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class SelfHostingPageRoutingModule {}
|
@ -0,0 +1,21 @@
|
||||
import { Component, OnDestroy } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
selector: 'gf-self-hosting-page',
|
||||
styleUrls: ['./self-hosting-page.scss'],
|
||||
templateUrl: './self-hosting-page.html'
|
||||
})
|
||||
export class SelfHostingPageComponent implements OnDestroy {
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor() {}
|
||||
|
||||
public ngOnInit() {}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
<div class="container">
|
||||
<div class="mb-5 row">
|
||||
<div class="col">
|
||||
<h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>
|
||||
Frequently Asked Questions (FAQ)
|
||||
</h1>
|
||||
<p>
|
||||
Find quick answers to commonly asked questions about self-hosting
|
||||
Ghostfolio in our Frequently Asked Questions (FAQ) section.
|
||||
</p>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title>How do I start?</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
If you prefer to run Ghostfolio on your own infrastructure, please
|
||||
find the source code and further instructions on
|
||||
<a href="https://github.com/ghostfolio/ghostfolio">GitHub</a>.
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Which devices are supported?</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content
|
||||
>Ghostfolio works in every modern web browser on smartphones, tablets
|
||||
and desktop computers.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Got any other questions?</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content
|
||||
>Please join the Ghostfolio
|
||||
<a
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
title="Join the Ghostfolio Slack community"
|
||||
>Slack </a
|
||||
>community, post to
|
||||
<a
|
||||
href="https://twitter.com/ghostfolio_"
|
||||
title="Post to Ghostfolio on X (formerly Twitter)"
|
||||
>@ghostfolio_</a
|
||||
>
|
||||
or start a discussion at
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio"
|
||||
title="Find Ghostfolio on GitHub"
|
||||
>GitHub</a
|
||||
>.</mat-card-content
|
||||
>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,13 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
|
||||
import { SelfHostingPageRoutingModule } from './self-hosting-page-routing.module';
|
||||
import { SelfHostingPageComponent } from './self-hosting-page.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [SelfHostingPageComponent],
|
||||
imports: [CommonModule, MatCardModule, SelfHostingPageRoutingModule],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class SelfHostingPageModule {}
|
@ -0,0 +1,12 @@
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
a {
|
||||
color: rgba(var(--palette-primary-500), 1);
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
color: rgba(var(--palette-primary-300), 1);
|
||||
}
|
||||
}
|
||||
}
|
@ -274,7 +274,7 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
public openUpdateActivityDialog(activity: Activity): void {
|
||||
public openUpdateActivityDialog(activity: Activity) {
|
||||
const dialogRef = this.dialog.open(CreateOrUpdateActivityDialog, {
|
||||
data: {
|
||||
activity,
|
||||
@ -311,7 +311,7 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
private openCreateActivityDialog(aActivity?: Activity): void {
|
||||
private openCreateActivityDialog(aActivity?: Activity) {
|
||||
this.userService
|
||||
.get()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
|
@ -150,7 +150,7 @@ export class ImportActivitiesDialog implements OnDestroy {
|
||||
}: {
|
||||
files: FileList;
|
||||
stepper: MatStepper;
|
||||
}): void {
|
||||
}) {
|
||||
if (files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
@ -24,84 +24,83 @@
|
||||
>
|
||||
</ng-template>
|
||||
<div class="pt-3">
|
||||
<ng-container *ngIf="mode === 'DIVIDEND'; else selectFile">
|
||||
<form
|
||||
[formGroup]="uniqueAssetForm"
|
||||
(ngSubmit)="onLoadDividends(stepper)"
|
||||
>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Holding</mat-label>
|
||||
<mat-select formControlName="uniqueAsset">
|
||||
<mat-select-trigger
|
||||
>{{ uniqueAssetForm.controls['uniqueAsset']?.value?.name
|
||||
}}</mat-select-trigger
|
||||
>
|
||||
<mat-option
|
||||
*ngFor="let holding of holdings"
|
||||
class="line-height-1"
|
||||
[value]="{ dataSource: holding.dataSource, name: holding.name, symbol: holding.symbol }"
|
||||
>
|
||||
<span><b>{{ holding.name }}</b></span>
|
||||
<br />
|
||||
<small class="text-muted"
|
||||
>{{ holding.symbol | gfSymbol }} · {{ holding.currency
|
||||
}}</small
|
||||
>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-spinner
|
||||
*ngIf="isLoading"
|
||||
class="position-absolute"
|
||||
[diameter]="20"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<div class="d-flex flex-column justify-content-center">
|
||||
<button
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
type="submit"
|
||||
[disabled]="!uniqueAssetForm.valid"
|
||||
@if (mode === 'DIVIDEND') {
|
||||
<form
|
||||
[formGroup]="uniqueAssetForm"
|
||||
(ngSubmit)="onLoadDividends(stepper)"
|
||||
>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Holding</mat-label>
|
||||
<mat-select formControlName="uniqueAsset">
|
||||
<mat-select-trigger
|
||||
>{{ uniqueAssetForm.controls['uniqueAsset']?.value?.name
|
||||
}}</mat-select-trigger
|
||||
>
|
||||
<span i18n>Load Dividends</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</ng-container>
|
||||
<ng-template #selectFile>
|
||||
<mat-option
|
||||
*ngFor="let holding of holdings"
|
||||
class="line-height-1"
|
||||
[value]="{ dataSource: holding.dataSource, name: holding.name, symbol: holding.symbol }"
|
||||
>
|
||||
<span><b>{{ holding.name }}</b></span>
|
||||
<br />
|
||||
<small class="text-muted"
|
||||
>{{ holding.symbol | gfSymbol }} · {{ holding.currency
|
||||
}}</small
|
||||
>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-spinner
|
||||
*ngIf="isLoading"
|
||||
class="position-absolute"
|
||||
[diameter]="20"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<div class="d-flex flex-column justify-content-center">
|
||||
<button
|
||||
class="drop-area p-4 text-center text-muted"
|
||||
gfFileDrop
|
||||
(click)="onSelectFile(stepper)"
|
||||
(filesDropped)="onFilesDropped({stepper, files: $event})"
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
type="submit"
|
||||
[disabled]="!uniqueAssetForm.valid"
|
||||
>
|
||||
<div
|
||||
class="align-items-center d-flex flex-column justify-content-center"
|
||||
>
|
||||
<ion-icon class="cloud-icon" name="cloud-upload-outline" />
|
||||
<span i18n>Choose or drop a file here</span>
|
||||
</div>
|
||||
<span i18n>Load Dividends</span>
|
||||
</button>
|
||||
<p class="mb-0 mt-3 text-center">
|
||||
<small>
|
||||
<span class="mr-1" i18n
|
||||
>The following file formats are supported:</span
|
||||
>
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.csv"
|
||||
target="_blank"
|
||||
>CSV</a
|
||||
>
|
||||
<span class="mx-1" i18n>or</span>
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.json"
|
||||
target="_blank"
|
||||
>JSON</a
|
||||
>
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
</ng-template>
|
||||
</form>
|
||||
} @else {
|
||||
<div class="d-flex flex-column justify-content-center">
|
||||
<button
|
||||
class="drop-area p-4 text-center text-muted"
|
||||
gfFileDrop
|
||||
(click)="onSelectFile(stepper)"
|
||||
(filesDropped)="onFilesDropped({stepper, files: $event})"
|
||||
>
|
||||
<div
|
||||
class="align-items-center d-flex flex-column justify-content-center"
|
||||
>
|
||||
<ion-icon class="cloud-icon" name="cloud-upload-outline" />
|
||||
<span i18n>Choose or drop a file here</span>
|
||||
</div>
|
||||
</button>
|
||||
<p class="mb-0 mt-3 text-center">
|
||||
<small>
|
||||
<span class="mr-1" i18n
|
||||
>The following file formats are supported:</span
|
||||
>
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.csv"
|
||||
target="_blank"
|
||||
>CSV</a
|
||||
>
|
||||
<span class="mx-1" i18n>or</span>
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.json"
|
||||
target="_blank"
|
||||
>JSON</a
|
||||
>
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</mat-step>
|
||||
|
||||
@ -115,79 +114,77 @@
|
||||
>
|
||||
</ng-template>
|
||||
<div class="pt-3">
|
||||
<ng-container *ngIf="errorMessages?.length === 0; else errorMessage">
|
||||
<gf-activities-table
|
||||
*ngIf="importStep === 1"
|
||||
[baseCurrency]="data?.user?.settings?.baseCurrency"
|
||||
[dataSource]="dataSource"
|
||||
[deviceType]="data?.deviceType"
|
||||
[hasPermissionToCreateActivity]="false"
|
||||
[hasPermissionToExportActivities]="false"
|
||||
[hasPermissionToFilter]="false"
|
||||
[hasPermissionToOpenDetails]="false"
|
||||
[locale]="data?.user?.settings?.locale"
|
||||
[pageSize]="maxSafeInteger"
|
||||
[showActions]="false"
|
||||
[showCheckbox]="true"
|
||||
[showFooter]="false"
|
||||
[showSymbolColumn]="false"
|
||||
[sortColumn]="sortColumn"
|
||||
[sortDirection]="sortDirection"
|
||||
[sortDisabled]="true"
|
||||
[totalItems]="totalItems"
|
||||
(selectedActivities)="updateSelection($event)"
|
||||
/>
|
||||
<div class="d-flex justify-content-end mt-3">
|
||||
<button mat-button (click)="onReset(stepper)">
|
||||
<ng-container i18n>Back</ng-container>
|
||||
</button>
|
||||
<button
|
||||
class="ml-1"
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
[disabled]="!selectedActivities?.length"
|
||||
(click)="onImportActivities()"
|
||||
>
|
||||
<ng-container i18n>Import</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #errorMessage>
|
||||
<mat-accordion displayMode="flat">
|
||||
<mat-expansion-panel
|
||||
*ngFor="let message of errorMessages; let i = index"
|
||||
[disabled]="!details[i]"
|
||||
>
|
||||
<mat-expansion-panel-header class="pl-1">
|
||||
<mat-panel-title>
|
||||
<div class="d-flex">
|
||||
<div class="align-items-center d-flex mr-2">
|
||||
<ion-icon name="warning-outline" />
|
||||
</div>
|
||||
<div>{{ message }}</div>
|
||||
@if(errorMessages?.length === 0) {
|
||||
<gf-activities-table
|
||||
*ngIf="importStep === 1"
|
||||
[baseCurrency]="data?.user?.settings?.baseCurrency"
|
||||
[dataSource]="dataSource"
|
||||
[deviceType]="data?.deviceType"
|
||||
[hasPermissionToCreateActivity]="false"
|
||||
[hasPermissionToExportActivities]="false"
|
||||
[hasPermissionToFilter]="false"
|
||||
[hasPermissionToOpenDetails]="false"
|
||||
[locale]="data?.user?.settings?.locale"
|
||||
[pageSize]="maxSafeInteger"
|
||||
[showActions]="false"
|
||||
[showCheckbox]="true"
|
||||
[showSymbolColumn]="false"
|
||||
[sortColumn]="sortColumn"
|
||||
[sortDirection]="sortDirection"
|
||||
[sortDisabled]="true"
|
||||
[totalItems]="totalItems"
|
||||
(selectedActivities)="updateSelection($event)"
|
||||
/>
|
||||
<div class="d-flex justify-content-end mt-3">
|
||||
<button mat-button (click)="onReset(stepper)">
|
||||
<ng-container i18n>Back</ng-container>
|
||||
</button>
|
||||
<button
|
||||
class="ml-1"
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
[disabled]="!selectedActivities?.length"
|
||||
(click)="onImportActivities()"
|
||||
>
|
||||
<ng-container i18n>Import</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
} @else {
|
||||
<mat-accordion displayMode="flat">
|
||||
<mat-expansion-panel
|
||||
*ngFor="let message of errorMessages; let i = index"
|
||||
[disabled]="!details[i]"
|
||||
>
|
||||
<mat-expansion-panel-header class="pl-1">
|
||||
<mat-panel-title>
|
||||
<div class="d-flex">
|
||||
<div class="align-items-center d-flex mr-2">
|
||||
<ion-icon name="warning-outline" />
|
||||
</div>
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<pre
|
||||
*ngIf="details[i]"
|
||||
class="m-0"
|
||||
><code>{{ details[i] | json }}</code></pre>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
<div class="d-flex justify-content-end mt-3">
|
||||
<button mat-button (click)="onReset(stepper)">
|
||||
<ng-container i18n>Back</ng-container>
|
||||
</button>
|
||||
<button
|
||||
class="ml-1"
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
[disabled]="true"
|
||||
>
|
||||
<ng-container i18n>Import</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
<div>{{ message }}</div>
|
||||
</div>
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<pre
|
||||
*ngIf="details[i]"
|
||||
class="m-0"
|
||||
><code>{{ details[i] | json }}</code></pre>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
<div class="d-flex justify-content-end mt-3">
|
||||
<button mat-button (click)="onReset(stepper)">
|
||||
<ng-container i18n>Back</ng-container>
|
||||
</button>
|
||||
<button
|
||||
class="ml-1"
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
[disabled]="true"
|
||||
>
|
||||
<ng-container i18n>Import</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</mat-step>
|
||||
</mat-stepper>
|
||||
|
@ -46,7 +46,9 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
public investmentTimelineDataLabel = $localize`Investment`;
|
||||
public investmentsByGroup: InvestmentItem[];
|
||||
public isLoadingBenchmarkComparator: boolean;
|
||||
public isLoadingDividendTimelineChart: boolean;
|
||||
public isLoadingInvestmentChart: boolean;
|
||||
public isLoadingInvestmentTimelineChart: boolean;
|
||||
public mode: GroupBy = 'month';
|
||||
public modeOptions: ToggleOption[] = [
|
||||
{ label: $localize`Monthly`, value: 'month' },
|
||||
@ -154,6 +156,9 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
private fetchDividendsAndInvestments() {
|
||||
this.isLoadingDividendTimelineChart = true;
|
||||
this.isLoadingInvestmentTimelineChart = true;
|
||||
|
||||
this.dataService
|
||||
.fetchDividends({
|
||||
filters: this.userService.getFilters(),
|
||||
@ -164,6 +169,8 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
.subscribe(({ dividends }) => {
|
||||
this.dividendsByGroup = dividends;
|
||||
|
||||
this.isLoadingDividendTimelineChart = false;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
@ -194,6 +201,8 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
? translate('MONTH')
|
||||
: translate('MONTHS');
|
||||
|
||||
this.isLoadingInvestmentTimelineChart = false;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
@ -294,6 +294,7 @@
|
||||
[daysInMarket]="daysInMarket"
|
||||
[groupBy]="mode"
|
||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||
[isLoading]="isLoadingInvestmentTimelineChart"
|
||||
[locale]="user?.settings?.locale"
|
||||
[range]="user?.settings?.dateRange"
|
||||
[savingsRate]="savingsRate"
|
||||
@ -331,6 +332,7 @@
|
||||
[daysInMarket]="daysInMarket"
|
||||
[groupBy]="mode"
|
||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||
[isLoading]="isLoadingDividendTimelineChart"
|
||||
[locale]="user?.settings?.locale"
|
||||
[range]="user?.settings?.dateRange"
|
||||
/>
|
||||
|
@ -48,11 +48,9 @@ export class FirePageComponent implements OnDestroy, OnInit {
|
||||
.fetchPortfolioDetails()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ summary }) => {
|
||||
if (summary.cash === null || summary.currentValue === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.fireWealth = new Big(summary.fireWealth);
|
||||
this.fireWealth = summary.fireWealth
|
||||
? new Big(summary.fireWealth)
|
||||
: new Big(10000);
|
||||
this.withdrawalRatePerYear = this.fireWealth.mul(4).div(100);
|
||||
this.withdrawalRatePerMonth = this.withdrawalRatePerYear.div(12);
|
||||
|
||||
@ -94,10 +92,13 @@ export class FirePageComponent implements OnDestroy, OnInit {
|
||||
permissions.createOrder
|
||||
);
|
||||
|
||||
this.hasPermissionToUpdateUserSettings = hasPermission(
|
||||
this.user.permissions,
|
||||
permissions.updateUserSettings
|
||||
);
|
||||
this.hasPermissionToUpdateUserSettings =
|
||||
this.user.subscription?.type === 'Basic'
|
||||
? false
|
||||
: hasPermission(
|
||||
this.user.permissions,
|
||||
permissions.updateUserSettings
|
||||
);
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
@ -18,6 +18,10 @@
|
||||
[fireWealth]="fireWealth?.toNumber()"
|
||||
[hasPermissionToUpdateUserSettings]="!hasImpersonationId && hasPermissionToUpdateUserSettings"
|
||||
[locale]="user?.settings?.locale"
|
||||
[ngStyle]="{
|
||||
opacity: user?.subscription?.type === 'Basic' ? '0.67' : 'initial',
|
||||
'pointer-events': user?.subscription?.type === 'Basic' ? 'none' : 'initial'
|
||||
}"
|
||||
[projectedTotalAmount]="user?.settings?.projectedTotalAmount"
|
||||
[retirementDate]="user?.settings?.retirementDate"
|
||||
[savingsRate]="user?.settings?.savingsRate"
|
||||
@ -54,7 +58,11 @@
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<div *ngIf="!isLoading" i18n>
|
||||
<div
|
||||
*ngIf="!isLoading"
|
||||
i18n
|
||||
[ngClass]="{ 'text-muted': user?.subscription?.type === 'Basic' }"
|
||||
>
|
||||
If you retire today, you would be able to withdraw
|
||||
<span class="font-weight-bold"
|
||||
><gf-value
|
||||
|
@ -132,6 +132,7 @@
|
||||
<div class="col-lg">
|
||||
<gf-holdings-table
|
||||
[deviceType]="deviceType"
|
||||
[hasPermissionToOpenDetails]="false"
|
||||
[hasPermissionToShowValues]="false"
|
||||
[holdings]="holdings"
|
||||
[pageSize]="7"
|
||||
|
@ -79,7 +79,7 @@ export class RegisterPageComponent implements OnDestroy, OnInit {
|
||||
accessToken: string,
|
||||
authToken: string,
|
||||
role: Role
|
||||
): void {
|
||||
) {
|
||||
const dialogRef = this.dialog.open(ShowAccessTokenDialog, {
|
||||
data: {
|
||||
accessToken,
|
||||
|
@ -18,7 +18,7 @@ export class SettingsStorageService {
|
||||
window.localStorage.setItem(aKey, aValue);
|
||||
}
|
||||
|
||||
public removeSetting(aKey: string): void {
|
||||
public removeSetting(aKey: string) {
|
||||
return window.localStorage.removeItem(aKey);
|
||||
}
|
||||
}
|
||||
|
@ -21,14 +21,14 @@ export class TokenStorageService {
|
||||
);
|
||||
}
|
||||
|
||||
public saveToken(token: string, staySignedIn = false): void {
|
||||
public saveToken(token: string, staySignedIn = false) {
|
||||
if (staySignedIn) {
|
||||
window.localStorage.setItem(KEY_TOKEN, token);
|
||||
}
|
||||
window.sessionStorage.setItem(KEY_TOKEN, token);
|
||||
}
|
||||
|
||||
public signOut(): void {
|
||||
public signOut() {
|
||||
const utmSource = window.localStorage.getItem('utm_source');
|
||||
|
||||
if (this.webAuthnService.isEnabled()) {
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -6,6 +6,7 @@ export interface AdminMarketData {
|
||||
}
|
||||
|
||||
export interface AdminMarketDataItem {
|
||||
activitiesCount?: number;
|
||||
assetClass?: AssetClass;
|
||||
assetSubClass?: AssetSubClass;
|
||||
countriesCount: number;
|
||||
|
@ -72,7 +72,7 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
public onAddFilter({ input, value }: MatChipInputEvent): void {
|
||||
public onAddFilter({ input, value }: MatChipInputEvent) {
|
||||
if (value?.trim()) {
|
||||
this.updateFilters();
|
||||
}
|
||||
@ -85,7 +85,7 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy {
|
||||
this.searchControl.setValue(undefined);
|
||||
}
|
||||
|
||||
public onRemoveFilter(aFilter: Filter): void {
|
||||
public onRemoveFilter(aFilter: Filter) {
|
||||
this.selectedFilters = this.selectedFilters.filter((filter) => {
|
||||
return filter.id !== aFilter.id;
|
||||
});
|
||||
@ -93,7 +93,7 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy {
|
||||
this.updateFilters();
|
||||
}
|
||||
|
||||
public onSelectFilter(event: MatAutocompleteSelectedEvent): void {
|
||||
public onSelectFilter(event: MatAutocompleteSelectedEvent) {
|
||||
this.selectedFilters.push(
|
||||
this.allFilters.find((filter) => {
|
||||
return filter.id === event.option.value;
|
||||
|
@ -45,7 +45,6 @@ export class ActivitiesTableComponent
|
||||
@Input() pageSize = DEFAULT_PAGE_SIZE;
|
||||
@Input() showActions = true;
|
||||
@Input() showCheckbox = false;
|
||||
@Input() showFooter = true;
|
||||
@Input() showNameColumn = true;
|
||||
@Input() sortColumn: string;
|
||||
@Input() sortDirection: SortDirection;
|
||||
@ -217,7 +216,7 @@ export class ActivitiesTableComponent
|
||||
alert(aComment);
|
||||
}
|
||||
|
||||
public onOpenPositionDialog({ dataSource, symbol }: UniqueAsset): void {
|
||||
public onOpenPositionDialog({ dataSource, symbol }: UniqueAsset) {
|
||||
this.router.navigate([], {
|
||||
queryParams: { dataSource, symbol, positionDetailDialog: true }
|
||||
});
|
||||
|
@ -10,7 +10,7 @@ export class CarouselItem implements FocusableOption {
|
||||
|
||||
public constructor(readonly element: ElementRef<HTMLElement>) {}
|
||||
|
||||
public focus(): void {
|
||||
public focus() {
|
||||
this.element.nativeElement.focus({ preventScroll: true });
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@
|
||||
.mdc-text-field--disabled {
|
||||
.mdc-floating-label,
|
||||
.mdc-text-field__input {
|
||||
color: inherit;
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
.mdc-notched-outline__leading,
|
||||
|
@ -137,12 +137,11 @@
|
||||
mat-row
|
||||
[ngClass]="{
|
||||
'cursor-pointer':
|
||||
hasPermissionToShowValues &&
|
||||
hasPermissionToOpenDetails &&
|
||||
!ignoreAssetSubClasses.includes(row.assetSubClass)
|
||||
}"
|
||||
(click)="
|
||||
hasPermissionToShowValues &&
|
||||
!ignoreAssetSubClasses.includes(row.assetSubClass) &&
|
||||
!ignoreAssetSubClasses.includes(row.assetSubClass) &&
|
||||
onOpenPositionDialog({
|
||||
dataSource: row.dataSource,
|
||||
symbol: row.symbol
|
||||
|
@ -26,6 +26,7 @@ export class HoldingsTableComponent implements OnChanges, OnDestroy, OnInit {
|
||||
@Input() baseCurrency: string;
|
||||
@Input() deviceType: string;
|
||||
@Input() hasPermissionToCreateActivity: boolean;
|
||||
@Input() hasPermissionToOpenDetails = true;
|
||||
@Input() hasPermissionToShowValues = true;
|
||||
@Input() holdings: PortfolioPosition[];
|
||||
@Input() locale: string;
|
||||
@ -68,10 +69,12 @@ export class HoldingsTableComponent implements OnChanges, OnDestroy, OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
public onOpenPositionDialog({ dataSource, symbol }: UniqueAsset): void {
|
||||
this.router.navigate([], {
|
||||
queryParams: { dataSource, symbol, positionDetailDialog: true }
|
||||
});
|
||||
public onOpenPositionDialog({ dataSource, symbol }: UniqueAsset) {
|
||||
if (this.hasPermissionToOpenDetails) {
|
||||
this.router.navigate([], {
|
||||
queryParams: { dataSource, symbol, positionDetailDialog: true }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public onShowAllPositions() {
|
||||
|
@ -134,31 +134,31 @@ export abstract class AbstractMatFormField<T>
|
||||
return this.focused || !this.empty;
|
||||
}
|
||||
|
||||
public ngDoCheck(): void {
|
||||
public ngDoCheck() {
|
||||
if (this.ngControl) {
|
||||
this.errorState = this.ngControl.invalid && this.ngControl.touched;
|
||||
this.stateChanges.next();
|
||||
}
|
||||
}
|
||||
|
||||
public ngOnDestroy(): void {
|
||||
public ngOnDestroy() {
|
||||
this.stateChanges.complete();
|
||||
this._focusMonitor.stopMonitoring(this._elementRef.nativeElement);
|
||||
}
|
||||
|
||||
public registerOnChange(fn: (_: T) => void): void {
|
||||
public registerOnChange(fn: (_: T) => void) {
|
||||
this.onChange = fn;
|
||||
}
|
||||
|
||||
public registerOnTouched(fn: () => void): void {
|
||||
public registerOnTouched(fn: () => void) {
|
||||
this.onTouched = fn;
|
||||
}
|
||||
|
||||
public setDescribedByIds(ids: string[]): void {
|
||||
public setDescribedByIds(ids: string[]) {
|
||||
this.describedBy = ids.join(' ');
|
||||
}
|
||||
|
||||
public writeValue(value: T): void {
|
||||
public writeValue(value: T) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@ -173,7 +173,7 @@ export abstract class AbstractMatFormField<T>
|
||||
this.stateChanges.next();
|
||||
}
|
||||
|
||||
public onContainerClick(): void {
|
||||
public onContainerClick() {
|
||||
if (!this.focused) {
|
||||
this.focus();
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ghostfolio",
|
||||
"version": "2.51.0",
|
||||
"version": "2.53.0",
|
||||
"homepage": "https://ghostfol.io",
|
||||
"license": "AGPL-3.0",
|
||||
"repository": "https://github.com/ghostfolio/ghostfolio",
|
||||
@ -116,7 +116,7 @@
|
||||
"lodash": "4.17.21",
|
||||
"marked": "9.1.6",
|
||||
"ms": "3.0.0-canary.1",
|
||||
"ng-extract-i18n-merge": "2.9.1",
|
||||
"ng-extract-i18n-merge": "2.10.0",
|
||||
"ngx-device-detector": "5.0.1",
|
||||
"ngx-markdown": "17.1.1",
|
||||
"ngx-skeleton-loader": "7.0.0",
|
||||
|
@ -15117,10 +15117,10 @@ neo-async@^2.5.0, neo-async@^2.6.2:
|
||||
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
|
||||
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
|
||||
|
||||
ng-extract-i18n-merge@2.9.1:
|
||||
version "2.9.1"
|
||||
resolved "https://registry.yarnpkg.com/ng-extract-i18n-merge/-/ng-extract-i18n-merge-2.9.1.tgz#f8e4431a8672d27be6508c72f3cf47a318b1ecab"
|
||||
integrity sha512-EJAgJrV2ZSRoH1njMI9lLLtLJkwabkk41ZZyV+U+6h8e5vDCM4zPGjm0NNZFy+YP+/ST+nlvi2CxprDXnjS8BQ==
|
||||
ng-extract-i18n-merge@2.10.0:
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/ng-extract-i18n-merge/-/ng-extract-i18n-merge-2.10.0.tgz#20d2a4a1d21f058773242cbcc8406c4ef0f8fea0"
|
||||
integrity sha512-mWYRWAUc7kirS3kIQxUR0kGv7Yv5JnV0C05VNvGwHdyMM3vSdJ0WAE/o4RwzW1cRyzXuG9oNOz4gctTzQsTErw==
|
||||
dependencies:
|
||||
"@angular-devkit/architect" "^0.1301.0 || ^0.1401.0 || ^0.1501.0 || ^0.1601.0 || ^0.1700.0"
|
||||
"@angular-devkit/core" "^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
|
||||
|
Reference in New Issue
Block a user