Browse Source

Merge branch 'ghostfolio:main' into main

pull/5027/head
dandevaud 2 years ago
committed by GitHub
parent
commit
be70c95ea1
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 31
      CHANGELOG.md
  2. 2
      DEVELOPMENT.md
  3. 2
      README.md
  4. 3
      apps/api/project.json
  5. 4
      apps/api/src/app/account/account.controller.ts
  6. 25
      apps/api/src/app/portfolio/portfolio.controller.ts
  7. 292
      apps/api/src/app/portfolio/portfolio.service.ts
  8. 8
      apps/api/src/app/subscription/subscription.service.ts
  9. 2
      apps/api/src/app/user/user.service.ts
  10. 6
      apps/api/src/decorators/has-permission.decorator.ts
  11. 55
      apps/api/src/guards/has-permission.guard.spec.ts
  12. 37
      apps/api/src/guards/has-permission.guard.ts
  13. 3
      apps/api/src/services/account-balance/account-balance.module.ts
  14. 52
      apps/api/src/services/account-balance/account-balance.service.ts
  15. 24
      apps/api/src/services/data-provider/coingecko/coingecko.service.ts
  16. 2
      apps/api/src/services/data-provider/data-provider.service.ts
  17. 16
      apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts
  18. 16
      apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts
  19. 8
      apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts
  20. 7
      apps/client/project.json
  21. 6
      apps/client/src/app/adapter/custom-date-adapter.ts
  22. 18
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts
  23. 25
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html
  24. 4
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.module.ts
  25. 4
      apps/client/src/app/pages/about/overview/about-overview-page.html
  26. 5
      apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.html
  27. 4
      apps/client/src/app/pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.html
  28. 6
      apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.html
  29. 4
      apps/client/src/app/pages/blog/2022/07/ghostfolio-meets-internet-identity/ghostfolio-meets-internet-identity-page.html
  30. 6
      apps/client/src/app/pages/blog/2022/08/500-stars-on-github/500-stars-on-github-page.html
  31. 4
      apps/client/src/app/pages/blog/2022/10/hacktoberfest-2022/hacktoberfest-2022-page.html
  32. 2
      apps/client/src/app/pages/blog/2023/02/ghostfolio-meets-umbrel/ghostfolio-meets-umbrel-page.html
  33. 2
      apps/client/src/app/pages/blog/2023/03/1000-stars-on-github/1000-stars-on-github-page.html
  34. 2
      apps/client/src/app/pages/blog/2023/09/hacktoberfest-2023/hacktoberfest-2023-page.html
  35. 12
      apps/client/src/app/pages/faq/faq-page.html
  36. 5
      apps/client/src/app/pages/i18n/i18n-page.html
  37. 2
      apps/client/src/app/pages/portfolio/activities/activities-page.html
  38. 2
      apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html
  39. 11
      apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html
  40. 1
      apps/client/src/app/pages/resources/personal-finance-tools/products.ts
  41. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/allvue-systems-page.component.ts
  42. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/altoo-page.component.ts
  43. 18
      apps/client/src/app/pages/resources/personal-finance-tools/products/base-page.component.ts
  44. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/basil-finance-page.component.ts
  45. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/beanvest-page.component.ts
  46. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/capitally-page.component.ts
  47. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/capmon-page.component.ts
  48. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/compound-planning-page.component.ts
  49. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/copilot-money-page.component.ts
  50. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/de.fi-page.component.ts
  51. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/delta-page.component.ts
  52. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/divvydiary-page.component.ts
  53. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/eightfigures-page.component.ts
  54. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/empower-page.component.ts
  55. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/exirio-page.component.ts
  56. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/finary-page.component.ts
  57. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/finwise-page.component.ts
  58. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/folishare-page.component.ts
  59. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/getquin-page.component.ts
  60. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/gospatz-page.component.ts
  61. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/intuit-mint-page.component.ts
  62. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/justetf-page.component.ts
  63. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/kubera-page.component.ts
  64. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/magnifi-page.component.ts
  65. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/markets.sh-page.component.ts
  66. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/maybe-finance-page.component.ts
  67. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/monarch-money-page.component.ts
  68. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/monse-page.component.ts
  69. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/parqet-page.component.ts
  70. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/plannix-page.component.ts
  71. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/portfolio-dividend-tracker-page.component.ts
  72. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/portseido-page.component.ts
  73. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/projectionlab-page.component.ts
  74. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/rocket-money-page.component.ts
  75. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/seeking-alpha-page.component.ts
  76. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/sharesight-page.component.ts
  77. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/simple-portfolio-page.component.ts
  78. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/snowball-analytics-page.component.ts
  79. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/stockle-page.component.ts
  80. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/stockmarketeye-page.component.ts
  81. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/sumio-page.component.ts
  82. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/tiller-page.component.ts
  83. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/utluna-page.component.ts
  84. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/vyzer-page.component.ts
  85. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/wealthica-page.component.ts
  86. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/whal-page.component.ts
  87. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/yeekatee-page.component.ts
  88. 3
      apps/client/src/app/pages/resources/personal-finance-tools/products/ynab-page.component.ts
  89. 7
      apps/client/src/app/services/data.service.ts
  90. 13
      apps/client/src/assets/oss-friends.json
  91. 1080
      apps/client/src/locales/messages.de.xlf
  92. 1080
      apps/client/src/locales/messages.es.xlf
  93. 1080
      apps/client/src/locales/messages.fr.xlf
  94. 1080
      apps/client/src/locales/messages.it.xlf
  95. 1080
      apps/client/src/locales/messages.nl.xlf
  96. 1080
      apps/client/src/locales/messages.pl.xlf
  97. 1080
      apps/client/src/locales/messages.pt.xlf
  98. 1048
      apps/client/src/locales/messages.tr.xlf
  99. 1046
      apps/client/src/locales/messages.xlf
  100. 3
      libs/common/project.json

31
CHANGELOG.md

@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Added
- Added a historical cash balances table to the account detail dialog
- Introduced a `HasPermission` annotation for endpoints
### Changed
- Respected the `withExcludedAccounts` flag in the account balance time series
### Fixed
- Changed the mechanism of the `INTRADAY` data gathering to operate synchronously avoiding database deadlocks
## 2.27.1 - 2023-11-28
### Changed
- Reverted `Nx` from version `17.1.3` to `17.0.2`
## 2.27.0 - 2023-11-26
### Changed
- Extended the chart in the account detail dialog by historical cash balances
- Improved the error log for a timeout in the data source request
- Improved the language localization for German (`de`)
- Upgraded `angular` from version `16.2.12` to `17.0.4`
- Upgraded `Nx` from version `17.0.2` to `17.1.3`
## 2.26.0 - 2023-11-24
### Changed ### Changed
- Upgraded `prisma` from version `5.5.2` to `5.6.0` - Upgraded `prisma` from version `5.5.2` to `5.6.0`

2
DEVELOPMENT.md

@ -32,7 +32,7 @@ Use `*ngIf="user?.settings?.isExperimentalFeatures"` in HTML template
1. Run `yarn nx migrate latest` 1. Run `yarn nx migrate latest`
1. Make sure `package.json` changes make sense and then run `yarn install` 1. Make sure `package.json` changes make sense and then run `yarn install`
1. Run `yarn nx migrate --run-migrations` 1. Run `yarn nx migrate --run-migrations` (Run `YARN_NODE_LINKER="node-modules" NX_MIGRATE_SKIP_INSTALL=1 yarn nx migrate --run-migrations` due to https://github.com/nrwl/nx/issues/16338)
### Prisma ### Prisma

2
README.md

@ -272,7 +272,7 @@ Are you building your own project? Add the `ghostfolio` topic to your _GitHub_ r
Ghostfolio is **100% free** and **open source**. We encourage and support an active and healthy community that accepts contributions from the public - including you. Ghostfolio is **100% free** and **open source**. We encourage and support an active and healthy community that accepts contributions from the public - including you.
Not sure what to work on? We have got some ideas. Please join the Ghostfolio [Slack](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg) channel or tweet to [@ghostfolio\_](https://twitter.com/ghostfolio_). We would love to hear from you. Not sure what to work on? We have got some ideas. Please join the Ghostfolio [Slack](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg) channel or post to [@ghostfolio\_](https://twitter.com/ghostfolio_) on _X_. We would love to hear from you.
If you like to support this project, get [**Ghostfolio Premium**](https://ghostfol.io/en/pricing) or [**Buy me a coffee**](https://www.buymeacoffee.com/ghostfolio). If you like to support this project, get [**Ghostfolio Premium**](https://ghostfol.io/en/pricing) or [**Buy me a coffee**](https://www.buymeacoffee.com/ghostfolio).

3
apps/api/project.json

@ -47,8 +47,7 @@
"test": { "test": {
"executor": "@nx/jest:jest", "executor": "@nx/jest:jest",
"options": { "options": {
"jestConfig": "apps/api/jest.config.ts", "jestConfig": "apps/api/jest.config.ts"
"passWithNoTests": true
}, },
"outputs": ["{workspaceRoot}/coverage/apps/api"] "outputs": ["{workspaceRoot}/coverage/apps/api"]
} }

4
apps/api/src/app/account/account.controller.ts

@ -128,8 +128,8 @@ export class AccountController {
@Param('id') id: string @Param('id') id: string
): Promise<AccountBalancesResponse> { ): Promise<AccountBalancesResponse> {
return this.accountBalanceService.getAccountBalances({ return this.accountBalanceService.getAccountBalances({
accountId: id, filters: [{ id, type: 'ACCOUNT' }],
userId: this.request.user.id user: this.request.user
}); });
} }

25
apps/api/src/app/portfolio/portfolio.controller.ts

@ -381,14 +381,32 @@ export class PortfolioController {
this.userService.isRestrictedView(this.request.user) this.userService.isRestrictedView(this.request.user)
) { ) {
performanceInformation.chart = performanceInformation.chart.map( performanceInformation.chart = performanceInformation.chart.map(
({ date, netPerformanceInPercentage, totalInvestment, value }) => { ({
date,
netPerformanceInPercentage,
netWorth,
totalInvestment,
value
}) => {
return { return {
date, date,
netPerformanceInPercentage, netPerformanceInPercentage,
totalInvestment: new Big(totalInvestment) netWorthInPercentage:
performanceInformation.performance.currentNetWorth === 0
? 0
: new Big(netWorth)
.div(performanceInformation.performance.currentNetWorth)
.toNumber(),
totalInvestment:
performanceInformation.performance.totalInvestment === 0
? 0
: new Big(totalInvestment)
.div(performanceInformation.performance.totalInvestment) .div(performanceInformation.performance.totalInvestment)
.toNumber(), .toNumber(),
valueInPercentage: new Big(value) valueInPercentage:
performanceInformation.performance.currentValue === 0
? 0
: new Big(value)
.div(performanceInformation.performance.currentValue) .div(performanceInformation.performance.currentValue)
.toNumber() .toNumber()
}; };
@ -400,6 +418,7 @@ export class PortfolioController {
[ [
'currentGrossPerformance', 'currentGrossPerformance',
'currentNetPerformance', 'currentNetPerformance',
'currentNetWorth',
'currentValue', 'currentValue',
'totalInvestment' 'totalInvestment'
] ]

292
apps/api/src/app/portfolio/portfolio.service.ts

@ -12,6 +12,7 @@ import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from '@ghostfolio/ap
import { CurrencyClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/current-investment'; import { CurrencyClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/current-investment';
import { EmergencyFundSetup } from '@ghostfolio/api/models/rules/emergency-fund/emergency-fund-setup'; import { EmergencyFundSetup } from '@ghostfolio/api/models/rules/emergency-fund/emergency-fund-setup';
import { FeeRatioInitialInvestment } from '@ghostfolio/api/models/rules/fees/fee-ratio-initial-investment'; import { FeeRatioInitialInvestment } from '@ghostfolio/api/models/rules/fees/fee-ratio-initial-investment';
import { AccountBalanceService } from '@ghostfolio/api/services/account-balance/account-balance.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
@ -67,7 +68,9 @@ import {
isBefore, isBefore,
isSameMonth, isSameMonth,
isSameYear, isSameYear,
isValid,
max, max,
min,
parseISO, parseISO,
set, set,
setDayOfYear, setDayOfYear,
@ -75,7 +78,7 @@ import {
subMonths, subMonths,
subYears subYears
} from 'date-fns'; } from 'date-fns';
import { isEmpty, sortBy, uniq, uniqBy } from 'lodash'; import { isEmpty, last, sortBy, uniq, uniqBy } from 'lodash';
import { import {
HistoricalDataContainer, HistoricalDataContainer,
@ -92,6 +95,7 @@ const europeMarkets = require('../../assets/countries/europe-markets.json');
@Injectable() @Injectable()
export class PortfolioService { export class PortfolioService {
public constructor( public constructor(
private readonly accountBalanceService: AccountBalanceService,
private readonly accountService: AccountService, private readonly accountService: AccountService,
private readonly currentRateService: CurrentRateService, private readonly currentRateService: CurrentRateService,
private readonly dataProviderService: DataProviderService, private readonly dataProviderService: DataProviderService,
@ -115,8 +119,12 @@ export class PortfolioService {
}): Promise<AccountWithValue[]> { }): Promise<AccountWithValue[]> {
const where: Prisma.AccountWhereInput = { userId: userId }; const where: Prisma.AccountWhereInput = { userId: userId };
if (filters?.[0].id && filters?.[0].type === 'ACCOUNT') { const accountFilter = filters?.find(({ type }) => {
where.id = filters[0].id; return type === 'ACCOUNT';
});
if (accountFilter) {
where.id = accountFilter.id;
} }
const [accounts, details] = await Promise.all([ const [accounts, details] = await Promise.all([
@ -268,6 +276,13 @@ export class PortfolioService {
includeDrafts: true includeDrafts: true
}); });
if (transactionPoints.length === 0) {
return {
investments: [],
streaks: { currentStreak: 0, longestStreak: 0 }
};
}
const portfolioCalculator = new PortfolioCalculator({ const portfolioCalculator = new PortfolioCalculator({
currency: this.request.user.Settings.settings.baseCurrency, currency: this.request.user.Settings.settings.baseCurrency,
currentRateService: this.currentRateService, currentRateService: this.currentRateService,
@ -275,12 +290,6 @@ export class PortfolioService {
}); });
portfolioCalculator.setTransactionPoints(transactionPoints); portfolioCalculator.setTransactionPoints(transactionPoints);
if (transactionPoints.length === 0) {
return {
investments: [],
streaks: { currentStreak: 0, longestStreak: 0 }
};
}
let investments: InvestmentItem[]; let investments: InvestmentItem[];
@ -368,67 +377,6 @@ export class PortfolioService {
}; };
} }
public async getChart({
dateRange = 'max',
filters,
impersonationId,
userCurrency,
userId,
withExcludedAccounts = false
}: {
dateRange?: DateRange;
filters?: Filter[];
impersonationId: string;
userCurrency: string;
userId: string;
withExcludedAccounts?: boolean;
}): Promise<HistoricalDataContainer> {
userId = await this.getUserId(impersonationId, userId);
const { portfolioOrders, transactionPoints } =
await this.getTransactionPoints({
filters,
userId,
withExcludedAccounts
});
const portfolioCalculator = new PortfolioCalculator({
currency: userCurrency,
currentRateService: this.currentRateService,
orders: portfolioOrders
});
portfolioCalculator.setTransactionPoints(transactionPoints);
if (transactionPoints.length === 0) {
return {
isAllTimeHigh: false,
isAllTimeLow: false,
items: []
};
}
const endDate = new Date();
const portfolioStart = parseDate(transactionPoints[0].date);
const startDate = this.getStartDate(dateRange, portfolioStart);
const daysInMarket = differenceInDays(new Date(), startDate);
const step = Math.round(
daysInMarket / Math.min(daysInMarket, MAX_CHART_ITEMS)
);
const items = await portfolioCalculator.getChartData(
startDate,
endDate,
step
);
return {
items,
isAllTimeHigh: false,
isAllTimeLow: false
};
}
public async getDetails({ public async getDetails({
dateRange = 'max', dateRange = 'max',
filters, filters,
@ -1118,12 +1066,6 @@ export class PortfolioService {
userId userId
}); });
const portfolioCalculator = new PortfolioCalculator({
currency: this.request.user.Settings.settings.baseCurrency,
currentRateService: this.currentRateService,
orders: portfolioOrders
});
if (transactionPoints?.length <= 0) { if (transactionPoints?.length <= 0) {
return { return {
hasErrors: false, hasErrors: false,
@ -1131,6 +1073,12 @@ export class PortfolioService {
}; };
} }
const portfolioCalculator = new PortfolioCalculator({
currency: this.request.user.Settings.settings.baseCurrency,
currentRateService: this.currentRateService,
orders: portfolioOrders
});
portfolioCalculator.setTransactionPoints(transactionPoints); portfolioCalculator.setTransactionPoints(transactionPoints);
const portfolioStart = parseDate(transactionPoints[0].date); const portfolioStart = parseDate(transactionPoints[0].date);
@ -1217,6 +1165,31 @@ export class PortfolioService {
const user = await this.userService.user({ id: userId }); const user = await this.userService.user({ id: userId });
const userCurrency = this.getUserCurrency(user); const userCurrency = this.getUserCurrency(user);
const accountBalances = await this.accountBalanceService.getAccountBalances(
{ filters, user, withExcludedAccounts }
);
let accountBalanceItems: HistoricalDataItem[] = Object.values(
// Reduce the array to a map with unique dates as keys
accountBalances.balances.reduce(
(
map: { [date: string]: HistoricalDataItem },
{ date, valueInBaseCurrency }
) => {
const formattedDate = format(date, DATE_FORMAT);
// Store the item in the map, overwriting if the date already exists
map[formattedDate] = {
date: formattedDate,
value: valueInBaseCurrency
};
return map;
},
{}
)
);
const { portfolioOrders, transactionPoints } = const { portfolioOrders, transactionPoints } =
await this.getTransactionPoints({ await this.getTransactionPoints({
filters, filters,
@ -1230,7 +1203,7 @@ export class PortfolioService {
orders: portfolioOrders orders: portfolioOrders
}); });
if (transactionPoints?.length <= 0) { if (accountBalanceItems?.length <= 0 && transactionPoints?.length <= 0) {
return { return {
chart: [], chart: [],
firstOrderDate: undefined, firstOrderDate: undefined,
@ -1240,6 +1213,7 @@ export class PortfolioService {
currentGrossPerformancePercent: 0, currentGrossPerformancePercent: 0,
currentNetPerformance: 0, currentNetPerformance: 0,
currentNetPerformancePercent: 0, currentNetPerformancePercent: 0,
currentNetWorth: 0,
currentValue: 0, currentValue: 0,
totalInvestment: 0 totalInvestment: 0
} }
@ -1248,7 +1222,15 @@ export class PortfolioService {
portfolioCalculator.setTransactionPoints(transactionPoints); portfolioCalculator.setTransactionPoints(transactionPoints);
const portfolioStart = parseDate(transactionPoints[0].date); const portfolioStart = min(
[
parseDate(accountBalanceItems[0]?.date),
parseDate(transactionPoints[0]?.date)
].filter((date) => {
return isValid(date);
})
);
const startDate = this.getStartDate(dateRange, portfolioStart); const startDate = this.getStartDate(dateRange, portfolioStart);
const { const {
currentValue, currentValue,
@ -1266,17 +1248,17 @@ export class PortfolioService {
let currentNetPerformance = netPerformance; let currentNetPerformance = netPerformance;
let currentNetPerformancePercent = netPerformancePercentage; let currentNetPerformancePercent = netPerformancePercentage;
const historicalDataContainer = await this.getChart({ const { items } = await this.getChart({
dateRange, dateRange,
filters,
impersonationId, impersonationId,
portfolioOrders,
transactionPoints,
userCurrency, userCurrency,
userId, userId
withExcludedAccounts
}); });
const itemOfToday = historicalDataContainer.items.find((item) => { const itemOfToday = items.find(({ date }) => {
return item.date === format(new Date(), DATE_FORMAT); return date === format(new Date(), DATE_FORMAT);
}); });
if (itemOfToday) { if (itemOfToday) {
@ -1286,34 +1268,42 @@ export class PortfolioService {
).div(100); ).div(100);
} }
accountBalanceItems = accountBalanceItems.filter(({ date }) => {
return !isBefore(parseDate(date), startDate);
});
const accountBalanceItemOfToday = accountBalanceItems.find(({ date }) => {
return date === format(new Date(), DATE_FORMAT);
});
if (!accountBalanceItemOfToday) {
accountBalanceItems.push({
date: format(new Date(), DATE_FORMAT),
value: last(accountBalanceItems)?.value ?? 0
});
}
const mergedHistoricalDataItems = this.mergeHistoricalDataItems(
accountBalanceItems,
items
);
const currentHistoricalDataItem = last(mergedHistoricalDataItems);
const currentNetWorth = currentHistoricalDataItem?.netWorth ?? 0;
return { return {
errors, errors,
hasErrors, hasErrors,
chart: historicalDataContainer.items.map( chart: mergedHistoricalDataItems,
({ firstOrderDate: parseDate(items[0]?.date),
date,
netPerformance: netPerformanceOfItem,
netPerformanceInPercentage,
totalInvestment: totalInvestmentOfItem,
value
}) => {
return {
date,
netPerformanceInPercentage,
value,
netPerformance: netPerformanceOfItem,
totalInvestment: totalInvestmentOfItem
};
}
),
firstOrderDate: parseDate(historicalDataContainer.items[0]?.date),
performance: { performance: {
currentValue: currentValue.toNumber(), currentNetWorth,
currentGrossPerformance: currentGrossPerformance.toNumber(), currentGrossPerformance: currentGrossPerformance.toNumber(),
currentGrossPerformancePercent: currentGrossPerformancePercent:
currentGrossPerformancePercent.toNumber(), currentGrossPerformancePercent.toNumber(),
currentNetPerformance: currentNetPerformance.toNumber(), currentNetPerformance: currentNetPerformance.toNumber(),
currentNetPerformancePercent: currentNetPerformancePercent.toNumber(), currentNetPerformancePercent: currentNetPerformancePercent.toNumber(),
currentValue: currentValue.toNumber(),
totalInvestment: totalInvestment.toNumber() totalInvestment: totalInvestment.toNumber()
} }
}; };
@ -1467,6 +1457,62 @@ export class PortfolioService {
return cashPositions; return cashPositions;
} }
private async getChart({
dateRange = 'max',
impersonationId,
portfolioOrders,
transactionPoints,
userCurrency,
userId
}: {
dateRange?: DateRange;
impersonationId: string;
portfolioOrders: PortfolioOrder[];
transactionPoints: TransactionPoint[];
userCurrency: string;
userId: string;
}): Promise<HistoricalDataContainer> {
if (transactionPoints.length === 0) {
return {
isAllTimeHigh: false,
isAllTimeLow: false,
items: []
};
}
userId = await this.getUserId(impersonationId, userId);
const portfolioCalculator = new PortfolioCalculator({
currency: userCurrency,
currentRateService: this.currentRateService,
orders: portfolioOrders
});
portfolioCalculator.setTransactionPoints(transactionPoints);
const endDate = new Date();
const portfolioStart = parseDate(transactionPoints[0].date);
const startDate = this.getStartDate(dateRange, portfolioStart);
const daysInMarket = differenceInDays(new Date(), startDate);
const step = Math.round(
daysInMarket / Math.min(daysInMarket, MAX_CHART_ITEMS)
);
const items = await portfolioCalculator.getChartData(
startDate,
endDate,
step
);
return {
items,
isAllTimeHigh: false,
isAllTimeLow: false
};
}
private getDividendsByGroup({ private getDividendsByGroup({
dividends, dividends,
groupBy groupBy
@ -2120,4 +2166,44 @@ export class PortfolioService {
return { accounts, platforms }; return { accounts, platforms };
} }
private mergeHistoricalDataItems(
accountBalanceItems: HistoricalDataItem[],
performanceChartItems: HistoricalDataItem[]
): HistoricalDataItem[] {
const historicalDataItemsMap: { [date: string]: HistoricalDataItem } = {};
let latestAccountBalance = 0;
for (const item of accountBalanceItems.concat(performanceChartItems)) {
const isAccountBalanceItem = accountBalanceItems.includes(item);
const totalAccountBalance = isAccountBalanceItem
? item.value
: latestAccountBalance;
if (isAccountBalanceItem && performanceChartItems.length > 0) {
latestAccountBalance = item.value;
} else {
historicalDataItemsMap[item.date] = {
...item,
totalAccountBalance,
netWorth:
(isAccountBalanceItem ? 0 : item.value) + totalAccountBalance
};
}
}
// Convert to an array and sort by date in ascending order
const historicalDataItems = Object.keys(historicalDataItemsMap).map(
(date) => {
return historicalDataItemsMap[date];
}
);
historicalDataItems.sort(
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
);
return historicalDataItems;
}
} }

8
apps/api/src/app/subscription/subscription.service.ts

@ -111,14 +111,14 @@ export class SubscriptionService {
aSubscriptions: Subscription[] aSubscriptions: Subscription[]
): UserWithSettings['subscription'] { ): UserWithSettings['subscription'] {
if (aSubscriptions.length > 0) { if (aSubscriptions.length > 0) {
const latestSubscription = aSubscriptions.reduce((a, b) => { const { expiresAt, price } = aSubscriptions.reduce((a, b) => {
return new Date(a.expiresAt) > new Date(b.expiresAt) ? a : b; return new Date(a.expiresAt) > new Date(b.expiresAt) ? a : b;
}); });
return { return {
expiresAt: latestSubscription.expiresAt, expiresAt,
offer: latestSubscription.price === 0 ? 'default' : 'renewal', offer: price ? 'renewal' : 'default',
type: isBefore(new Date(), latestSubscription.expiresAt) type: isBefore(new Date(), expiresAt)
? SubscriptionType.Premium ? SubscriptionType.Premium
: SubscriptionType.Basic : SubscriptionType.Basic
}; };

2
apps/api/src/app/user/user.service.ts

@ -60,7 +60,7 @@ export class UserService {
PROPERTY_SYSTEM_MESSAGE PROPERTY_SYSTEM_MESSAGE
)) as SystemMessage; )) as SystemMessage;
if (systemMessageProperty?.targetGroups?.includes(subscription.type)) { if (systemMessageProperty?.targetGroups?.includes(subscription?.type)) {
systemMessage = systemMessageProperty; systemMessage = systemMessageProperty;
} }

6
apps/api/src/decorators/has-permission.decorator.ts

@ -0,0 +1,6 @@
import { SetMetadata } from '@nestjs/common';
export const HAS_PERMISSION_KEY = 'has_permission';
export function HasPermission(permission: string) {
return SetMetadata(HAS_PERMISSION_KEY, permission);
}

55
apps/api/src/guards/has-permission.guard.spec.ts

@ -0,0 +1,55 @@
import { HttpException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host';
import { Test, TestingModule } from '@nestjs/testing';
import { HasPermissionGuard } from './has-permission.guard';
describe('HasPermissionGuard', () => {
let guard: HasPermissionGuard;
let reflector: Reflector;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [HasPermissionGuard, Reflector]
}).compile();
guard = module.get<HasPermissionGuard>(HasPermissionGuard);
reflector = module.get<Reflector>(Reflector);
});
function setupReflectorSpy(returnValue: string) {
jest.spyOn(reflector, 'get').mockReturnValue(returnValue);
}
function createMockExecutionContext(permissions: string[]) {
return new ExecutionContextHost([
{
user: {
permissions // Set user permissions based on the argument
}
}
]);
}
it('should deny access if the user does not have any permission', () => {
setupReflectorSpy('required-permission');
const noPermissions = createMockExecutionContext([]);
expect(() => guard.canActivate(noPermissions)).toThrow(HttpException);
});
it('should deny access if the user has the wrong permission', () => {
setupReflectorSpy('required-permission');
const wrongPermission = createMockExecutionContext(['wrong-permission']);
expect(() => guard.canActivate(wrongPermission)).toThrow(HttpException);
});
it('should allow access if the user has the required permission', () => {
setupReflectorSpy('required-permission');
const rightPermission = createMockExecutionContext(['required-permission']);
expect(guard.canActivate(rightPermission)).toBe(true);
});
});

37
apps/api/src/guards/has-permission.guard.ts

@ -0,0 +1,37 @@
import { HAS_PERMISSION_KEY } from '@ghostfolio/api/decorators/has-permission.decorator';
import { hasPermission } from '@ghostfolio/common/permissions';
import {
CanActivate,
ExecutionContext,
HttpException,
Injectable
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
@Injectable()
export class HasPermissionGuard implements CanActivate {
public constructor(private reflector: Reflector) {}
public canActivate(context: ExecutionContext): boolean {
const requiredPermission = this.reflector.get<string>(
HAS_PERMISSION_KEY,
context.getHandler()
);
if (!requiredPermission) {
return true; // No specific permissions required
}
const { user } = context.switchToHttp().getRequest();
if (!user || !hasPermission(user.permissions, requiredPermission)) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return true;
}
}

3
apps/api/src/services/account-balance/account-balance.module.ts

@ -1,10 +1,11 @@
import { AccountBalanceService } from '@ghostfolio/api/services/account-balance/account-balance.service'; import { AccountBalanceService } from '@ghostfolio/api/services/account-balance/account-balance.service';
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
@Module({ @Module({
exports: [AccountBalanceService], exports: [AccountBalanceService],
imports: [PrismaModule], imports: [ExchangeRateDataModule, PrismaModule],
providers: [AccountBalanceService] providers: [AccountBalanceService]
}) })
export class AccountBalanceModule {} export class AccountBalanceModule {}

52
apps/api/src/services/account-balance/account-balance.service.ts

@ -1,11 +1,16 @@
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { AccountBalancesResponse } from '@ghostfolio/common/interfaces'; import { AccountBalancesResponse, Filter } from '@ghostfolio/common/interfaces';
import { UserWithSettings } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { AccountBalance, Prisma } from '@prisma/client'; import { AccountBalance, Prisma } from '@prisma/client';
@Injectable() @Injectable()
export class AccountBalanceService { export class AccountBalanceService {
public constructor(private readonly prismaService: PrismaService) {} public constructor(
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly prismaService: PrismaService
) {}
public async createAccountBalance( public async createAccountBalance(
data: Prisma.AccountBalanceCreateInput data: Prisma.AccountBalanceCreateInput
@ -16,27 +21,52 @@ export class AccountBalanceService {
} }
public async getAccountBalances({ public async getAccountBalances({
accountId, filters,
userId user,
withExcludedAccounts
}: { }: {
accountId: string; filters?: Filter[];
userId: string; user: UserWithSettings;
withExcludedAccounts?: boolean;
}): Promise<AccountBalancesResponse> { }): Promise<AccountBalancesResponse> {
const where: Prisma.AccountBalanceWhereInput = { userId: user.id };
const accountFilter = filters?.find(({ type }) => {
return type === 'ACCOUNT';
});
if (accountFilter) {
where.accountId = accountFilter.id;
}
if (withExcludedAccounts === false) {
where.Account = { isExcluded: false };
}
const balances = await this.prismaService.accountBalance.findMany({ const balances = await this.prismaService.accountBalance.findMany({
where,
orderBy: { orderBy: {
date: 'asc' date: 'asc'
}, },
select: { select: {
Account: true,
date: true, date: true,
id: true, id: true,
value: true value: true
},
where: {
accountId,
userId
} }
}); });
return { balances }; return {
balances: balances.map((balance) => {
return {
...balance,
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
balance.value,
balance.Account.currency,
user.Settings.settings.baseCurrency
)
};
})
};
} }
} }

24
apps/api/src/services/data-provider/coingecko/coingecko.service.ts

@ -56,7 +56,13 @@ export class CoinGeckoService implements DataProviderInterface {
response.name = name; response.name = name;
} catch (error) { } catch (error) {
Logger.error(error, 'CoinGeckoService'); let message = error;
if (error?.code === 'ABORT_ERR') {
message = `RequestError: The operation was aborted because the request to the data provider took more than ${DEFAULT_REQUEST_TIMEOUT}ms`;
}
Logger.error(message, 'CoinGeckoService');
} }
return response; return response;
@ -174,7 +180,13 @@ export class CoinGeckoService implements DataProviderInterface {
}; };
} }
} catch (error) { } catch (error) {
Logger.error(error, 'CoinGeckoService'); let message = error;
if (error?.code === 'ABORT_ERR') {
message = `RequestError: The operation was aborted because the request to the data provider took more than ${DEFAULT_REQUEST_TIMEOUT}ms`;
}
Logger.error(message, 'CoinGeckoService');
} }
return response; return response;
@ -216,7 +228,13 @@ export class CoinGeckoService implements DataProviderInterface {
}; };
}); });
} catch (error) { } catch (error) {
Logger.error(error, 'CoinGeckoService'); let message = error;
if (error?.code === 'ABORT_ERR') {
message = `RequestError: The operation was aborted because the request to the data provider took more than ${DEFAULT_REQUEST_TIMEOUT}ms`;
}
Logger.error(message, 'CoinGeckoService');
} }
return { items }; return { items };

2
apps/api/src/services/data-provider/data-provider.service.ts

@ -346,7 +346,7 @@ export class DataProviderService {
); );
try { try {
this.marketDataService.updateMany({ await this.marketDataService.updateMany({
data: Object.keys(response) data: Object.keys(response)
.filter((symbol) => { .filter((symbol) => {
return ( return (

16
apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts

@ -229,7 +229,13 @@ export class EodHistoricalDataService implements DataProviderInterface {
return response; return response;
} catch (error) { } catch (error) {
Logger.error(error, 'EodHistoricalDataService'); let message = error;
if (error?.code === 'ABORT_ERR') {
message = `RequestError: The operation was aborted because the request to the data provider took more than ${DEFAULT_REQUEST_TIMEOUT}ms`;
}
Logger.error(message, 'EodHistoricalDataService');
} }
return {}; return {};
@ -382,7 +388,13 @@ export class EodHistoricalDataService implements DataProviderInterface {
} }
); );
} catch (error) { } catch (error) {
Logger.error(error, 'EodHistoricalDataService'); let message = error;
if (error?.code === 'ABORT_ERR') {
message = `RequestError: The operation was aborted because the request to the data provider took more than ${DEFAULT_REQUEST_TIMEOUT}ms`;
}
Logger.error(message, 'EodHistoricalDataService');
} }
return searchResult; return searchResult;

16
apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts

@ -151,7 +151,13 @@ export class FinancialModelingPrepService implements DataProviderInterface {
}; };
} }
} catch (error) { } catch (error) {
Logger.error(error, 'FinancialModelingPrepService'); let message = error;
if (error?.code === 'ABORT_ERR') {
message = `RequestError: The operation was aborted because the request to the data provider took more than ${DEFAULT_REQUEST_TIMEOUT}ms`;
}
Logger.error(message, 'FinancialModelingPrepService');
} }
return response; return response;
@ -196,7 +202,13 @@ export class FinancialModelingPrepService implements DataProviderInterface {
}; };
}); });
} catch (error) { } catch (error) {
Logger.error(error, 'FinancialModelingPrepService'); let message = error;
if (error?.code === 'ABORT_ERR') {
message = `RequestError: The operation was aborted because the request to the data provider took more than ${DEFAULT_REQUEST_TIMEOUT}ms`;
}
Logger.error(message, 'FinancialModelingPrepService');
} }
return { items }; return { items };

8
apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts

@ -163,7 +163,13 @@ export class RapidApiService implements DataProviderInterface {
return fgi; return fgi;
} catch (error) { } catch (error) {
Logger.error(error, 'RapidApiService'); let message = error;
if (error?.code === 'ABORT_ERR') {
message = `RequestError: The operation was aborted because the request to the data provider took more than ${DEFAULT_REQUEST_TIMEOUT}ms`;
}
Logger.error(message, 'RapidApiService');
return undefined; return undefined;
} }

7
apps/client/project.json

@ -152,8 +152,8 @@
"serve": { "serve": {
"executor": "@nx/angular:webpack-dev-server", "executor": "@nx/angular:webpack-dev-server",
"options": { "options": {
"browserTarget": "client:build", "proxyConfig": "apps/client/proxy.conf.json",
"proxyConfig": "apps/client/proxy.conf.json" "browserTarget": "client:build"
}, },
"configurations": { "configurations": {
"development-de": { "development-de": {
@ -215,8 +215,7 @@
"test": { "test": {
"executor": "@nx/jest:jest", "executor": "@nx/jest:jest",
"options": { "options": {
"jestConfig": "apps/client/jest.config.ts", "jestConfig": "apps/client/jest.config.ts"
"passWithNoTests": true
}, },
"outputs": ["{workspaceRoot}/coverage/apps/client"] "outputs": ["{workspaceRoot}/coverage/apps/client"]
} }

6
apps/client/src/app/adapter/custom-date-adapter.ts

@ -1,4 +1,3 @@
import { Platform } from '@angular/cdk/platform';
import { Inject, forwardRef } from '@angular/core'; import { Inject, forwardRef } from '@angular/core';
import { MAT_DATE_LOCALE, NativeDateAdapter } from '@angular/material/core'; import { MAT_DATE_LOCALE, NativeDateAdapter } from '@angular/material/core';
import { getDateFormatString } from '@ghostfolio/common/helper'; import { getDateFormatString } from '@ghostfolio/common/helper';
@ -7,10 +6,9 @@ import { addYears, format, getYear, parse } from 'date-fns';
export class CustomDateAdapter extends NativeDateAdapter { export class CustomDateAdapter extends NativeDateAdapter {
public constructor( public constructor(
@Inject(MAT_DATE_LOCALE) public locale: string, @Inject(MAT_DATE_LOCALE) public locale: string,
@Inject(forwardRef(() => MAT_DATE_LOCALE)) matDateLocale: string, @Inject(forwardRef(() => MAT_DATE_LOCALE)) matDateLocale: string
platform: Platform
) { ) {
super(matDateLocale, platform); super(matDateLocale);
} }
/** /**

18
apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts

@ -29,14 +29,15 @@ import { AccountDetailDialogParams } from './interfaces/interfaces';
styleUrls: ['./account-detail-dialog.component.scss'] styleUrls: ['./account-detail-dialog.component.scss']
}) })
export class AccountDetailDialog implements OnDestroy, OnInit { export class AccountDetailDialog implements OnDestroy, OnInit {
public activities: OrderWithAccount[];
public balance: number; public balance: number;
public currency: string; public currency: string;
public equity: number; public equity: number;
public hasImpersonationId: boolean; public hasImpersonationId: boolean;
public historicalDataItems: HistoricalDataItem[]; public historicalDataItems: HistoricalDataItem[];
public isLoadingActivities: boolean;
public isLoadingChart: boolean; public isLoadingChart: boolean;
public name: string; public name: string;
public orders: OrderWithAccount[];
public platformName: string; public platformName: string;
public transactionCount: number; public transactionCount: number;
public user: User; public user: User;
@ -64,6 +65,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
} }
public ngOnInit() { public ngOnInit() {
this.isLoadingActivities = true;
this.isLoadingChart = true; this.isLoadingChart = true;
this.dataService this.dataService
@ -103,7 +105,9 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ activities }) => { .subscribe(({ activities }) => {
this.orders = activities; this.activities = activities;
this.isLoadingActivities = false;
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
@ -122,13 +126,13 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ chart }) => { .subscribe(({ chart }) => {
this.historicalDataItems = chart.map( this.historicalDataItems = chart.map(
({ date, value, valueInPercentage }) => { ({ date, netWorth, netWorthInPercentage }) => {
return { return {
date, date,
value: value:
this.hasImpersonationId || this.user.settings.isRestrictedView this.hasImpersonationId || this.user.settings.isRestrictedView
? valueInPercentage ? netWorthInPercentage
: value : netWorth
}; };
} }
); );
@ -153,8 +157,8 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
public onExport() { public onExport() {
this.dataService this.dataService
.fetchExport( .fetchExport(
this.orders.map((order) => { this.activities.map(({ id }) => {
return order.id; return id;
}) })
) )
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))

25
apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html

@ -31,7 +31,7 @@
></gf-investment-chart> ></gf-investment-chart>
</div> </div>
<div class="row"> <div class="mb-3 row">
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value <gf-value
i18n i18n
@ -64,11 +64,15 @@
</div> </div>
</div> </div>
<div class="row" [ngClass]="{ 'd-none': !orders?.length }"> <mat-tab-group
<div class="col mb-3"> animationDuration="0"
<div class="h5 mb-0" i18n>Activities</div> [mat-stretch-tabs]="false"
[ngClass]="{ 'd-none': isLoadingActivities }"
>
<mat-tab>
<ng-template i18n mat-tab-label>Activities</ng-template>
<gf-activities-table <gf-activities-table
[activities]="orders" [activities]="activities"
[baseCurrency]="user?.settings?.baseCurrency" [baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="data.deviceType" [deviceType]="data.deviceType"
[hasPermissionToCreateActivity]="false" [hasPermissionToCreateActivity]="false"
@ -79,8 +83,15 @@
[showActions]="false" [showActions]="false"
(export)="onExport()" (export)="onExport()"
></gf-activities-table> ></gf-activities-table>
</div> </mat-tab>
</div> <mat-tab>
<ng-template i18n mat-tab-label>Cash Balances</ng-template>
<gf-account-balances
[accountId]="data.accountId"
[locale]="user?.settings?.locale"
></gf-account-balances>
</mat-tab>
</mat-tab-group>
</div> </div>
</div> </div>

4
apps/client/src/app/components/account-detail-dialog/account-detail-dialog.module.ts

@ -2,9 +2,11 @@ import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { MatTabsModule } from '@angular/material/tabs';
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module'; import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module'; import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module'; import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module';
import { GfAccountBalancesModule } from '@ghostfolio/ui/account-balances/account-balances.module';
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module'; import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
import { GfValueModule } from '@ghostfolio/ui/value'; import { GfValueModule } from '@ghostfolio/ui/value';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@ -15,6 +17,7 @@ import { AccountDetailDialog } from './account-detail-dialog.component';
declarations: [AccountDetailDialog], declarations: [AccountDetailDialog],
imports: [ imports: [
CommonModule, CommonModule,
GfAccountBalancesModule,
GfActivitiesTableModule, GfActivitiesTableModule,
GfDialogFooterModule, GfDialogFooterModule,
GfDialogHeaderModule, GfDialogHeaderModule,
@ -22,6 +25,7 @@ import { AccountDetailDialog } from './account-detail-dialog.component';
GfValueModule, GfValueModule,
MatButtonModule, MatButtonModule,
MatDialogModule, MatDialogModule,
MatTabsModule,
NgxSkeletonLoaderModule NgxSkeletonLoaderModule
], ],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]

4
apps/client/src/app/pages/about/overview/about-overview-page.html

@ -56,11 +56,11 @@
<a <a
href="https://twitter.com/ghostfolio_" href="https://twitter.com/ghostfolio_"
title="Post to Ghostfolio on X (formerly Twitter)" title="Post to Ghostfolio on X (formerly Twitter)"
>@ghostfolio_</a >&#64;ghostfolio_</a
><ng-container *ngIf="user?.subscription?.type === 'Premium'" ><ng-container *ngIf="user?.subscription?.type === 'Premium'"
>, send an e-mail to >, send an e-mail to
<a href="mailto:hi@ghostfol.io" title="Send an e-mail" <a href="mailto:hi@ghostfol.io" title="Send an e-mail"
>hi@ghostfol.io</a >hi&#64;ghostfol.io</a
></ng-container ></ng-container
> >
or start a discussion at or start a discussion at

5
apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.html

@ -131,8 +131,9 @@
</p> </p>
<p> <p>
Du erreichst mich per E-Mail unter Du erreichst mich per E-Mail unter
<a href="mailto:hi@ghostfol.io">hi@ghostfol.io</a> oder auf Twitter <a href="mailto:hi@ghostfol.io">hi&#64;ghostfol.io</a> oder auf
<a href="https://twitter.com/ghostfolio_">@ghostfolio_</a>. Twitter
<a href="https://twitter.com/ghostfolio_">&#64;ghostfolio_</a>.
</p> </p>
<p> <p>
Ich freue mich, von dir zu hören.<br /> Ich freue mich, von dir zu hören.<br />

4
apps/client/src/app/pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.html

@ -126,8 +126,8 @@
</p> </p>
<p> <p>
You can reach me by e-mail at You can reach me by e-mail at
<a href="mailto:hi@ghostfol.io">hi@ghostfol.io</a> or on Twitter <a href="mailto:hi@ghostfol.io">hi&#64;ghostfol.io</a> or on Twitter
<a href="https://twitter.com/ghostfolio_">@ghostfolio_</a>. <a href="https://twitter.com/ghostfolio_">&#64;ghostfolio_</a>.
</p> </p>
<p> <p>
I look forward to hearing from you.<br /> I look forward to hearing from you.<br />

6
apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.html

@ -100,9 +100,9 @@
of users. In the future, I would like to involve more contributors of users. In the future, I would like to involve more contributors
to further extend the functionality of Ghostfolio (e.g. with new to further extend the functionality of Ghostfolio (e.g. with new
reports). Get in touch with me by e-mail at reports). Get in touch with me by e-mail at
<a href="mailto:hi@ghostfol.io">hi@ghostfol.io</a> or on Twitter <a href="mailto:hi@ghostfol.io">hi&#64;ghostfol.io</a> or on Twitter
<a href="https://twitter.com/ghostfolio_">@ghostfolio_</a> if you <a href="https://twitter.com/ghostfolio_">&#64;ghostfolio_</a> if
are interested, I’m happy to discuss ideas. you are interested, I’m happy to discuss ideas.
</p> </p>
<p> <p>
I would like to say thank you for all your feedback and support I would like to say thank you for all your feedback and support

4
apps/client/src/app/pages/blog/2022/07/ghostfolio-meets-internet-identity/ghostfolio-meets-internet-identity-page.html

@ -90,8 +90,8 @@
<p> <p>
If you would like to provide feedback or get involved in further If you would like to provide feedback or get involved in further
development of Ghostfolio, please get in touch by e-mail via development of Ghostfolio, please get in touch by e-mail via
<a href="mailto:hi@ghostfol.io">hi@ghostfol.io</a> or on Twitter <a href="mailto:hi@ghostfol.io">hi&#64;ghostfol.io</a> or on Twitter
<a href="https://twitter.com/ghostfolio_">@ghostfolio_</a>. <a href="https://twitter.com/ghostfolio_">&#64;ghostfolio_</a>.
</p> </p>
<p> <p>
I look forward to hearing from you.<br /> I look forward to hearing from you.<br />

6
apps/client/src/app/pages/blog/2022/08/500-stars-on-github/500-stars-on-github-page.html

@ -91,9 +91,9 @@
engineering to realize the full potential of open source software. engineering to realize the full potential of open source software.
If you are a web developer and interested in personal finance, If you are a web developer and interested in personal finance,
please get in touch by e-mail via please get in touch by e-mail via
<a href="mailto:hi@ghostfol.io">hi@ghostfol.io</a> or on Twitter <a href="mailto:hi@ghostfol.io">hi&#64;ghostfol.io</a> or on Twitter
<a href="https://twitter.com/ghostfolio_">@ghostfolio_</a>. We are <a href="https://twitter.com/ghostfolio_">&#64;ghostfolio_</a>. We
happy to discuss ideas. are happy to discuss ideas.
</p> </p>
<p> <p>
We would like to say thank you for all your feedback and support We would like to say thank you for all your feedback and support

4
apps/client/src/app/pages/blog/2022/10/hacktoberfest-2022/hacktoberfest-2022-page.html

@ -85,8 +85,8 @@
>Slack</a >Slack</a
> >
community or get in touch on Twitter community or get in touch on Twitter
<a href="https://twitter.com/ghostfolio_">@ghostfolio_</a> or by <a href="https://twitter.com/ghostfolio_">&#64;ghostfolio_</a> or by
e-mail via <a href="mailto:hi@ghostfol.io">hi@ghostfol.io</a>. e-mail via <a href="mailto:hi@ghostfol.io">hi&#64;ghostfol.io</a>.
</p> </p>
<p> <p>
We look forward to hearing from you.<br /> We look forward to hearing from you.<br />

2
apps/client/src/app/pages/blog/2023/02/ghostfolio-meets-umbrel/ghostfolio-meets-umbrel-page.html

@ -92,7 +92,7 @@
> >
community or via Twitter community or via Twitter
<a href="https://twitter.com/ghostfolio_" target="_blank" <a href="https://twitter.com/ghostfolio_" target="_blank"
>@ghostfolio_</a >&#64;ghostfolio_</a
>. We look forward to hearing from you! >. We look forward to hearing from you!
</p> </p>
</section> </section>

2
apps/client/src/app/pages/blog/2023/03/1000-stars-on-github/1000-stars-on-github-page.html

@ -122,7 +122,7 @@
>Slack</a >Slack</a
> >
community or connect with community or connect with
<a href="https://twitter.com/ghostfolio_">@ghostfolio_</a> on <a href="https://twitter.com/ghostfolio_">&#64;ghostfolio_</a> on
Twitter. We are happy to discuss ideas and get you involved. Twitter. We are happy to discuss ideas and get you involved.
</p> </p>
<p>Thank you for all your feedback and support.</p> <p>Thank you for all your feedback and support.</p>

2
apps/client/src/app/pages/blog/2023/09/hacktoberfest-2023/hacktoberfest-2023-page.html

@ -89,7 +89,7 @@
>Slack</a >Slack</a
> >
community or get in touch on X community or get in touch on X
<a href="https://twitter.com/ghostfolio_">@ghostfolio_</a>. <a href="https://twitter.com/ghostfolio_">&#64;ghostfolio_</a>.
</p> </p>
<p> <p>
We look forward to hearing from you.<br /> We look forward to hearing from you.<br />

12
apps/client/src/app/pages/faq/faq-page.html

@ -203,8 +203,8 @@
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
Please send an e-mail with the web address of your broker to 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 <a href="mailto:hi@ghostfol.io">hi&#64;ghostfol.io</a> and we are
add it. happy to add it.
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
<mat-card appearance="outlined" class="mb-3"> <mat-card appearance="outlined" class="mb-3">
@ -234,11 +234,11 @@
<a <a
href="https://twitter.com/ghostfolio_" href="https://twitter.com/ghostfolio_"
title="Post to Ghostfolio on X (formerly Twitter)" title="Post to Ghostfolio on X (formerly Twitter)"
>@ghostfolio_</a >&#64;ghostfolio_</a
><ng-container *ngIf="user?.subscription?.type === 'Premium'" ><ng-container *ngIf="user?.subscription?.type === 'Premium'"
>, >,
<a href="mailto:hi@ghostfol.io" title="Send an e-mail" <a href="mailto:hi@ghostfol.io" title="Send an e-mail"
>hi@ghostfol.io</a >hi&#64;ghostfol.io</a
></ng-container ></ng-container
> >
or or
@ -263,11 +263,11 @@
<a <a
href="https://twitter.com/ghostfolio_" href="https://twitter.com/ghostfolio_"
title="Post to Ghostfolio on X (formerly Twitter)" title="Post to Ghostfolio on X (formerly Twitter)"
>@ghostfolio_</a >&#64;ghostfolio_</a
><ng-container *ngIf="user?.subscription?.type === 'Premium'" ><ng-container *ngIf="user?.subscription?.type === 'Premium'"
>, send an e-mail to >, send an e-mail to
<a href="mailto:hi@ghostfol.io" title="Send an e-mail" <a href="mailto:hi@ghostfol.io" title="Send an e-mail"
>hi@ghostfol.io</a >hi&#64;ghostfol.io</a
></ng-container ></ng-container
> >
or start a discussion at or start a discussion at

5
apps/client/src/app/pages/i18n/i18n-page.html

@ -2,8 +2,9 @@
<div class="row"> <div class="row">
<ul> <ul>
<li i18n="@@metaDescription"> <li i18n="@@metaDescription">
Ghostfolio is a personal finance dashboard to keep track of your assets Ghostfolio is a personal finance dashboard to keep track of your net
like stocks, ETFs or cryptocurrencies across multiple platforms. worth including cash, stocks, ETFs and cryptocurrencies across multiple
platforms.
</li> </li>
<li i18n="@@metaKeywords"> <li i18n="@@metaKeywords">
app, asset, cryptocurrency, dashboard, etf, finance, management, app, asset, cryptocurrency, dashboard, etf, finance, management,

2
apps/client/src/app/pages/portfolio/activities/activities-page.html

@ -1,5 +1,5 @@
<div class="container"> <div class="container">
<div class="row mb-3"> <div class="mb-3 row">
<div class="col"> <div class="col">
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Activities</h1> <h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Activities</h1>
<gf-activities-table <gf-activities-table

2
apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html

@ -33,7 +33,7 @@
place. If I lose it, I cannot get my account back. place. If I lose it, I cannot get my account back.
</p> </p>
</div> </div>
<div class="float-right" mat-dialog-actions> <div class="justify-content-end" mat-dialog-actions>
<button i18n mat-flat-button [mat-dialog-close]="undefined">Cancel</button> <button i18n mat-flat-button [mat-dialog-close]="undefined">Cancel</button>
<button <button
color="primary" color="primary"

11
apps/client/src/app/pages/resources/personal-finance-tools/product-page-template.html

@ -181,13 +181,14 @@
</tr> </tr>
<tr class="mat-mdc-row"> <tr class="mat-mdc-row">
<td class="mat-mdc-cell px-3 py-2 text-right" i18n>Pricing</td> <td class="mat-mdc-cell px-3 py-2 text-right" i18n>Pricing</td>
<td class="mat-mdc-cell px-1 py-2" i18n> <td class="mat-mdc-cell px-1 py-2">
Starting from {{ product1.pricingPerYear }} / year <span i18n>Starting from</span> ${{ price }} /
<span i18n>year</span>
</td> </td>
<td class="mat-mdc-cell px-1 py-2"> <td class="mat-mdc-cell px-1 py-2">
<ng-container *ngIf="product2.pricingPerYear" i18n <ng-container *ngIf="product2.pricingPerYear"
>Starting from {{ product2.pricingPerYear }} / ><span i18n>Starting from</span> {{ product2.pricingPerYear
year</ng-container }} / <span i18n>year</span></ng-container
> >
</td> </td>
</tr> </tr>

1
apps/client/src/app/pages/resources/personal-finance-tools/products.ts

@ -67,7 +67,6 @@ export const products: Product[] = [
], ],
name: 'Ghostfolio', name: 'Ghostfolio',
origin: $localize`Switzerland`, origin: $localize`Switzerland`,
pricingPerYear: '$24',
region: $localize`Global`, region: $localize`Global`,
slogan: 'Open Source Wealth Management', slogan: 'Open Source Wealth Management',
useAnonymously: true useAnonymously: true

3
apps/client/src/app/pages/resources/personal-finance-tools/products/allvue-systems-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class AllvueSystemsPageComponent { export class AllvueSystemsPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/altoo-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class AltooPageComponent { export class AltooPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

18
apps/client/src/app/pages/resources/personal-finance-tools/products/base-page.component.ts

@ -0,0 +1,18 @@
import { Component, OnInit } from '@angular/core';
import { DataService } from '@ghostfolio/client/services/data.service';
@Component({
selector: 'gf-base-product-page',
template: ''
})
export class BaseProductPageComponent implements OnInit {
public price: number;
public constructor(private dataService: DataService) {}
public ngOnInit() {
const { subscriptions } = this.dataService.fetchInfo();
this.price = subscriptions?.default?.price;
}
}

3
apps/client/src/app/pages/resources/personal-finance-tools/products/basil-finance-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class BasilFinancePageComponent { export class BasilFinancePageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/beanvest-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class BeanvestPageComponent { export class BeanvestPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/capitally-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class CapitallyPageComponent { export class CapitallyPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/capmon-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class CapMonPageComponent { export class CapMonPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/compound-planning-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class CompoundPlanningPageComponent { export class CompoundPlanningPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/copilot-money-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class CopilotMoneyPageComponent { export class CopilotMoneyPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/de.fi-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class DeFiPageComponent { export class DeFiPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/delta-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class DeltaPageComponent { export class DeltaPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/divvydiary-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class DivvyDiaryPageComponent { export class DivvyDiaryPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/eightfigures-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class EightFiguresPageComponent { export class EightFiguresPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/empower-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class EmpowerPageComponent { export class EmpowerPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/exirio-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class ExirioPageComponent { export class ExirioPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/finary-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class FinaryPageComponent { export class FinaryPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/finwise-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class FinWisePageComponent { export class FinWisePageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/folishare-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class FolisharePageComponent { export class FolisharePageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/getquin-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class GetquinPageComponent { export class GetquinPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/gospatz-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class GoSpatzPageComponent { export class GoSpatzPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/intuit-mint-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class IntuitMintPageComponent { export class IntuitMintPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/justetf-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class JustEtfPageComponent { export class JustEtfPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/kubera-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class KuberaPageComponent { export class KuberaPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/magnifi-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class MagnifiPageComponent { export class MagnifiPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/markets.sh-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class MarketsShPageComponent { export class MarketsShPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/maybe-finance-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class MaybeFinancePageComponent { export class MaybeFinancePageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/monarch-money-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class MonarchMoneyPageComponent { export class MonarchMoneyPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/monse-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class MonsePageComponent { export class MonsePageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/parqet-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class ParqetPageComponent { export class ParqetPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/plannix-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class PlannixPageComponent { export class PlannixPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/portfolio-dividend-tracker-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class PortfolioDividendTrackerPageComponent { export class PortfolioDividendTrackerPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/portseido-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class PortseidoPageComponent { export class PortseidoPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/projectionlab-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class ProjectionLabPageComponent { export class ProjectionLabPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/rocket-money-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class RocketMoneyPageComponent { export class RocketMoneyPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/seeking-alpha-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class SeekingAlphaPageComponent { export class SeekingAlphaPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/sharesight-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class SharesightPageComponent { export class SharesightPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/simple-portfolio-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class SimplePortfolioPageComponent { export class SimplePortfolioPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/snowball-analytics-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class SnowballAnalyticsPageComponent { export class SnowballAnalyticsPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/stockle-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class StocklePageComponent { export class StocklePageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/stockmarketeye-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class StockMarketEyePageComponent { export class StockMarketEyePageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/sumio-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class SumioPageComponent { export class SumioPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/tiller-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class TillerPageComponent { export class TillerPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/utluna-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class UtlunaPageComponent { export class UtlunaPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/vyzer-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class VyzerPageComponent { export class VyzerPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/wealthica-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class WealthicaPageComponent { export class WealthicaPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/whal-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class WhalPageComponent { export class WhalPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/yeekatee-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class YeekateePageComponent { export class YeekateePageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

3
apps/client/src/app/pages/resources/personal-finance-tools/products/ynab-page.component.ts

@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { products } from '../products'; import { products } from '../products';
import { BaseProductPageComponent } from './base-page.component';
@Component({ @Component({
host: { class: 'page' }, host: { class: 'page' },
@ -13,7 +14,7 @@ import { products } from '../products';
styleUrls: ['../product-page-template.scss'], styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html' templateUrl: '../product-page-template.html'
}) })
export class YnabPageComponent { export class YnabPageComponent extends BaseProductPageComponent {
public product1 = products.find(({ key }) => { public product1 = products.find(({ key }) => {
return key === 'ghostfolio'; return key === 'ghostfolio';
}); });

7
apps/client/src/app/services/data.service.ts

@ -18,6 +18,7 @@ import { PropertyDto } from '@ghostfolio/api/services/property/property.dto';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { import {
Access, Access,
AccountBalancesResponse,
Accounts, Accounts,
BenchmarkMarketDataDetails, BenchmarkMarketDataDetails,
BenchmarkResponse, BenchmarkResponse,
@ -137,6 +138,12 @@ export class DataService {
return this.http.get<AccountWithValue>(`/api/v1/account/${aAccountId}`); return this.http.get<AccountWithValue>(`/api/v1/account/${aAccountId}`);
} }
public fetchAccountBalances(aAccountId: string) {
return this.http.get<AccountBalancesResponse>(
`/api/v1/account/${aAccountId}/balances`
);
}
public fetchAccounts() { public fetchAccounts() {
return this.http.get<Accounts>('/api/v1/account'); return this.http.get<Accounts>('/api/v1/account');
} }

13
apps/client/src/assets/oss-friends.json

@ -1,5 +1,5 @@
{ {
"createdAt": "2023-11-17T00:00:00.000Z", "createdAt": "2023-11-30T00:00:00.000Z",
"data": [ "data": [
{ {
"name": "BoxyHQ", "name": "BoxyHQ",
@ -16,6 +16,11 @@
"description": "Centralize community, product, and customer data to understand which companies are engaging with your open source project.", "description": "Centralize community, product, and customer data to understand which companies are engaging with your open source project.",
"href": "https://www.crowd.dev" "href": "https://www.crowd.dev"
}, },
{
"name": "DevHunt",
"description": "Find the best Dev Tools upvoted by the community every week.",
"href": "https://devhunt.org"
},
{ {
"name": "Documenso", "name": "Documenso",
"description": "The Open-Source DocuSign Alternative. We aim to earn your trust by enabling you to self-host the platform and examine its inner workings.", "description": "The Open-Source DocuSign Alternative. We aim to earn your trust by enabling you to self-host the platform and examine its inner workings.",
@ -59,7 +64,7 @@
{ {
"name": "Hook0", "name": "Hook0",
"description": "Open-Source Webhooks-as-a-service (WaaS) that makes it easy for developers to send webhooks.", "description": "Open-Source Webhooks-as-a-service (WaaS) that makes it easy for developers to send webhooks.",
"href": "https://www.hook0.com/" "href": "https://www.hook0.com"
}, },
{ {
"name": "HTMX", "name": "HTMX",
@ -89,7 +94,7 @@
{ {
"name": "Papermark", "name": "Papermark",
"description": "Open-Source Docsend Alternative to securely share documents with real-time analytics.", "description": "Open-Source Docsend Alternative to securely share documents with real-time analytics.",
"href": "https://www.papermark.io/" "href": "https://www.papermark.io"
}, },
{ {
"name": "Requestly", "name": "Requestly",
@ -109,7 +114,7 @@
{ {
"name": "Shelf.nu", "name": "Shelf.nu",
"description": "Open Source Asset and Equipment tracking software that lets you create QR asset labels, manage and overview your assets across locations.", "description": "Open Source Asset and Equipment tracking software that lets you create QR asset labels, manage and overview your assets across locations.",
"href": "https://www.shelf.nu/" "href": "https://www.shelf.nu"
}, },
{ {
"name": "Sniffnet", "name": "Sniffnet",

1080
apps/client/src/locales/messages.de.xlf

File diff suppressed because it is too large

1080
apps/client/src/locales/messages.es.xlf

File diff suppressed because it is too large

1080
apps/client/src/locales/messages.fr.xlf

File diff suppressed because it is too large

1080
apps/client/src/locales/messages.it.xlf

File diff suppressed because it is too large

1080
apps/client/src/locales/messages.nl.xlf

File diff suppressed because it is too large

1080
apps/client/src/locales/messages.pl.xlf

File diff suppressed because it is too large

1080
apps/client/src/locales/messages.pt.xlf

File diff suppressed because it is too large

1048
apps/client/src/locales/messages.tr.xlf

File diff suppressed because it is too large

1046
apps/client/src/locales/messages.xlf

File diff suppressed because it is too large

3
libs/common/project.json

@ -14,8 +14,7 @@
"executor": "@nx/jest:jest", "executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/libs/common"], "outputs": ["{workspaceRoot}/coverage/libs/common"],
"options": { "options": {
"jestConfig": "libs/common/jest.config.ts", "jestConfig": "libs/common/jest.config.ts"
"passWithNoTests": true
} }
} }
}, },

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

Loading…
Cancel
Save