Compare commits
52 Commits
Author | SHA1 | Date | |
---|---|---|---|
aa078588e8 | |||
fcef0a72d5 | |||
29987d3e2f | |||
6284b4dfe8 | |||
00342ca1f7 | |||
234c4fd511 | |||
669f1fb60c | |||
52df0c62ab | |||
e8e1bb83bf | |||
45510702d0 | |||
1b7e3a1e47 | |||
35f98b9d2d | |||
e980aed9e7 | |||
d993067e9a | |||
3d09bfdb0c | |||
3fbc4f500f | |||
373201a98f | |||
681f88f002 | |||
8a523a981a | |||
81ded53363 | |||
5272407af8 | |||
c48f89d117 | |||
046fdd3ae7 | |||
e69c7a753c | |||
5191415b5a | |||
a704378702 | |||
cf7ce64de7 | |||
8c1b45f35b | |||
6ad1528d01 | |||
4a6fbe4d30 | |||
e31741f0c7 | |||
b26aa7f51d | |||
c0fccd186f | |||
a7baad10d1 | |||
16f1b16e41 | |||
409ddc90ce | |||
95bc84956e | |||
20cefaba19 | |||
379c651ce0 | |||
7804c6879d | |||
de2255f9ba | |||
e4ec5f213e | |||
f3c2fb853d | |||
f5ad1d2d24 | |||
0af37ca1d7 | |||
2992a0da4c | |||
2dcc7e161c | |||
fa627f686f | |||
0567083fc1 | |||
3212efef17 | |||
6077e7c2f9 | |||
96b5dcfaf8 |
1
.github/workflows/docker-image.yml
vendored
1
.github/workflows/docker-image.yml
vendored
@ -21,6 +21,7 @@ jobs:
|
||||
with:
|
||||
images: ghostfolio/ghostfolio
|
||||
tags: |
|
||||
type=semver,pattern={{major}}
|
||||
type=semver,pattern={{version}}
|
||||
|
||||
- name: Set up QEMU
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -27,6 +27,7 @@
|
||||
/.angular/cache
|
||||
.env
|
||||
.env.prod
|
||||
.nx/cache
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
|
@ -1,2 +1,3 @@
|
||||
/.nx/cache
|
||||
/dist
|
||||
/test/import
|
||||
|
82
CHANGELOG.md
82
CHANGELOG.md
@ -5,6 +5,88 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## 2.19.0 - 2023-11-06
|
||||
|
||||
### Added
|
||||
|
||||
- Added a data migration to set `accountType` to `NULL` in the account database table
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the language localization for the _Fear & Greed Index_ (market mood)
|
||||
- Improved the language localization for German (`de`)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improved the handling of derived currencies (`GBp`, `ILA`, `ZAc`)
|
||||
|
||||
## 2.18.0 - 2023-11-05
|
||||
|
||||
### Added
|
||||
|
||||
- Added support to import activities by `isin` in the _Yahoo Finance_ service
|
||||
- Added a new tag with the major version to the docker image on _Docker Hub_
|
||||
- Added a blog post: _Hacktoberfest 2023 Debriefing_
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgraded `angular` from version `16.2.1` to `16.2.12`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue to get quotes in the _CoinGecko_ service
|
||||
- Loosened the validation in the activities import (expects values greater than or equal to 0 for `fee`, `quantity` and `unitPrice`)
|
||||
- Handled an issue with a failing database query (`account.findMany()`) related to activities without account
|
||||
|
||||
## 2.17.0 - 2023-11-02
|
||||
|
||||
### Added
|
||||
|
||||
- Added a button to edit the exchange rates in the admin control panel
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the language localization for German (`de`)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue in the biometric authentication
|
||||
- Fixed the alignment of the icons in various menus
|
||||
|
||||
## 2.16.0 - 2023-10-29
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the check for duplicates in the preview step of the activities import (allow different accounts)
|
||||
- Improved the usability and validation in the cash balance transfer from one to another account
|
||||
- Changed the checkboxes to slide toggles in the overview of the admin control panel
|
||||
- Switched from the deprecated (`PUT`) to the new endpoint (`POST`) to manage historical market data in the asset profile details dialog of the admin control panel
|
||||
- Improved the date parsing in the import historical market data of the admin control panel
|
||||
- Improved the localized meta data (keywords) in `html` files
|
||||
- Improved the language localization for German (`de`)
|
||||
- Upgraded `prisma` from version `5.4.2` to `5.5.2`
|
||||
|
||||
## 2.15.0 - 2023-10-26
|
||||
|
||||
### Added
|
||||
|
||||
- Added support to edit the name, asset class and asset sub class of asset profiles with `MANUAL` data source in the asset profile details dialog of the admin control panel
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the style and wording of the position detail dialog
|
||||
- Improved the validation in the activities import (expects positive values for `fee`, `quantity` and `unitPrice`)
|
||||
- Improved the validation in the cash balance transfer from one to another account (expects a positive value)
|
||||
- Changed the currency selector in the create or update account dialog to `@angular/material/autocomplete`
|
||||
- Upgraded `Nx` from version `16.7.4` to `17.0.2`
|
||||
- Upgraded `uuid` from version `9.0.0` to `9.0.1`
|
||||
- Upgraded `yahoo-finance2` from version `2.8.0` to `2.8.1`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the chart in the account detail dialog for accounts excluded from analysis
|
||||
- Verified the current benchmark before loading it on the analysis page
|
||||
|
||||
## 2.14.0 - 2023-10-21
|
||||
|
||||
### Added
|
||||
|
@ -20,6 +20,12 @@ Use `*ngIf="user?.settings?.isExperimentalFeatures"` in HTML template
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Angular
|
||||
|
||||
#### Upgrade (minor versions)
|
||||
|
||||
1. Run `npx npm-check-updates --upgrade --target "minor" --filter "/@angular.*/"`
|
||||
|
||||
### Nx
|
||||
|
||||
#### Upgrade
|
||||
|
24
README.md
24
README.md
@ -230,18 +230,18 @@ Deprecated: `GET http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TO
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
| ---------- | ------------------- | -------------------------------------------------- |
|
||||
| accountId | string (`optional`) | Id of the account |
|
||||
| comment | string (`optional`) | Comment of the activity |
|
||||
| currency | string | `CHF` \| `EUR` \| `USD` etc. |
|
||||
| dataSource | string | `MANUAL` (for type `ITEM`) \| `YAHOO` |
|
||||
| date | string | Date in the format `ISO-8601` |
|
||||
| fee | number | Fee of the activity |
|
||||
| quantity | number | Quantity of the activity |
|
||||
| symbol | string | Symbol of the activity (suitable for `dataSource`) |
|
||||
| type | string | `BUY` \| `DIVIDEND` \| `ITEM` \| `SELL` |
|
||||
| unitPrice | number | Price per unit of the activity |
|
||||
| Field | Type | Description |
|
||||
| ---------- | ------------------- | ----------------------------------------------------------------------------- |
|
||||
| accountId | string (`optional`) | Id of the account |
|
||||
| comment | string (`optional`) | Comment of the activity |
|
||||
| currency | string | `CHF` \| `EUR` \| `USD` etc. |
|
||||
| dataSource | string | `COINGECKO` \| `MANUAL` (for type `ITEM`) \| `YAHOO` |
|
||||
| date | string | Date in the format `ISO-8601` |
|
||||
| fee | number | Fee of the activity |
|
||||
| quantity | number | Quantity of the activity |
|
||||
| symbol | string | Symbol of the activity (suitable for `dataSource`) |
|
||||
| type | string | `BUY` \| `DIVIDEND` \| `FEE` \| `INTEREST` \| `ITEM` \| `LIABILITY` \| `SELL` |
|
||||
| unitPrice | number | Price per unit of the activity |
|
||||
|
||||
#### Response
|
||||
|
||||
|
@ -190,36 +190,46 @@ export class AccountController {
|
||||
this.request.user.id
|
||||
);
|
||||
|
||||
const currentAccountIds = accountsOfUser.map(({ id }) => {
|
||||
return id;
|
||||
const accountFrom = accountsOfUser.find(({ id }) => {
|
||||
return id === accountIdFrom;
|
||||
});
|
||||
|
||||
if (
|
||||
![accountIdFrom, accountIdTo].every((accountId) => {
|
||||
return currentAccountIds.includes(accountId);
|
||||
})
|
||||
) {
|
||||
const accountTo = accountsOfUser.find(({ id }) => {
|
||||
return id === accountIdTo;
|
||||
});
|
||||
|
||||
if (!accountFrom || !accountTo) {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.NOT_FOUND),
|
||||
StatusCodes.NOT_FOUND
|
||||
);
|
||||
}
|
||||
|
||||
const { currency } = accountsOfUser.find(({ id }) => {
|
||||
return id === accountIdFrom;
|
||||
});
|
||||
if (accountFrom.id === accountTo.id) {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.BAD_REQUEST),
|
||||
StatusCodes.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
if (accountFrom.balance < balance) {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.BAD_REQUEST),
|
||||
StatusCodes.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
await this.accountService.updateAccountBalance({
|
||||
currency,
|
||||
accountId: accountIdFrom,
|
||||
accountId: accountFrom.id,
|
||||
amount: -balance,
|
||||
currency: accountFrom.currency,
|
||||
userId: this.request.user.id
|
||||
});
|
||||
|
||||
await this.accountService.updateAccountBalance({
|
||||
currency,
|
||||
accountId: accountIdTo,
|
||||
accountId: accountTo.id,
|
||||
amount: balance,
|
||||
currency: accountFrom.currency,
|
||||
userId: this.request.user.id
|
||||
});
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { IsNumber, IsString } from 'class-validator';
|
||||
import { IsNumber, IsPositive, IsString } from 'class-validator';
|
||||
|
||||
export class TransferBalanceDto {
|
||||
@IsString()
|
||||
@ -8,5 +8,6 @@ export class TransferBalanceDto {
|
||||
accountIdTo: string;
|
||||
|
||||
@IsNumber()
|
||||
@IsPositive()
|
||||
balance: number;
|
||||
}
|
||||
|
@ -7,7 +7,10 @@ import {
|
||||
GATHER_ASSET_PROFILE_PROCESS,
|
||||
GATHER_ASSET_PROFILE_PROCESS_OPTIONS
|
||||
} from '@ghostfolio/common/config';
|
||||
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
getAssetProfileIdentifier,
|
||||
resetHours
|
||||
} from '@ghostfolio/common/helper';
|
||||
import {
|
||||
AdminData,
|
||||
AdminMarketData,
|
||||
@ -331,9 +334,9 @@ export class AdminController {
|
||||
const dataBulkUpdate: Prisma.MarketDataUpdateInput[] = data.marketData.map(
|
||||
({ date, marketPrice }) => ({
|
||||
dataSource,
|
||||
date,
|
||||
marketPrice,
|
||||
symbol,
|
||||
date: resetHours(parseISO(date)),
|
||||
state: 'CLOSE'
|
||||
})
|
||||
);
|
||||
|
@ -23,7 +23,13 @@ import {
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { MarketDataPreset } from '@ghostfolio/common/types';
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { AssetSubClass, Prisma, Property, SymbolProfile } from '@prisma/client';
|
||||
import {
|
||||
AssetSubClass,
|
||||
DataSource,
|
||||
Prisma,
|
||||
Property,
|
||||
SymbolProfile
|
||||
} from '@prisma/client';
|
||||
import { differenceInDays } from 'date-fns';
|
||||
import { groupBy } from 'lodash';
|
||||
|
||||
@ -94,9 +100,17 @@ export class AdminService {
|
||||
return currency !== DEFAULT_CURRENCY;
|
||||
})
|
||||
.map((currency) => {
|
||||
const label1 = DEFAULT_CURRENCY;
|
||||
const label2 = currency;
|
||||
|
||||
return {
|
||||
label1: DEFAULT_CURRENCY,
|
||||
label2: currency,
|
||||
label1,
|
||||
label2,
|
||||
dataSource:
|
||||
DataSource[
|
||||
this.configurationService.get('DATA_SOURCE_EXCHANGE_RATES')
|
||||
],
|
||||
symbol: `${label1}${label2}`,
|
||||
value: this.exchangeRateDataService.toCurrency(
|
||||
1,
|
||||
DEFAULT_CURRENCY,
|
||||
@ -303,15 +317,21 @@ export class AdminService {
|
||||
}
|
||||
|
||||
public async patchAssetProfileData({
|
||||
assetClass,
|
||||
assetSubClass,
|
||||
comment,
|
||||
dataSource,
|
||||
name,
|
||||
scraperConfiguration,
|
||||
symbol,
|
||||
symbolMapping
|
||||
}: Prisma.SymbolProfileUpdateInput & UniqueAsset) {
|
||||
await this.symbolProfileService.updateSymbolProfile({
|
||||
assetClass,
|
||||
assetSubClass,
|
||||
comment,
|
||||
dataSource,
|
||||
name,
|
||||
scraperConfiguration,
|
||||
symbol,
|
||||
symbolMapping
|
||||
|
@ -1,11 +1,23 @@
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { IsObject, IsOptional, IsString } from 'class-validator';
|
||||
import { AssetClass, AssetSubClass, Prisma } from '@prisma/client';
|
||||
import { IsEnum, IsObject, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class UpdateAssetProfileDto {
|
||||
@IsEnum(AssetClass, { each: true })
|
||||
@IsOptional()
|
||||
assetClass?: AssetClass;
|
||||
|
||||
@IsEnum(AssetSubClass, { each: true })
|
||||
@IsOptional()
|
||||
assetSubClass?: AssetSubClass;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
comment?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
name?: string;
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
scraperConfiguration?: Prisma.InputJsonObject;
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { IsDate, IsNumber, IsOptional } from 'class-validator';
|
||||
import { IsISO8601, IsNumber, IsOptional } from 'class-validator';
|
||||
|
||||
export class UpdateMarketDataDto {
|
||||
@IsDate()
|
||||
@IsISO8601()
|
||||
@IsOptional()
|
||||
date?: Date;
|
||||
date?: string;
|
||||
|
||||
@IsNumber()
|
||||
marketPrice: number;
|
||||
|
@ -83,6 +83,7 @@ export class ImportService {
|
||||
|
||||
const isDuplicate = orders.some((activity) => {
|
||||
return (
|
||||
activity.accountId === Account?.id &&
|
||||
activity.SymbolProfile.currency === assetProfile.currency &&
|
||||
activity.SymbolProfile.dataSource === assetProfile.dataSource &&
|
||||
isSameDay(activity.date, parseDate(dateString)) &&
|
||||
@ -482,6 +483,7 @@ export class ImportService {
|
||||
const date = parseISO(<string>(<unknown>dateString));
|
||||
const isDuplicate = existingActivities.some((activity) => {
|
||||
return (
|
||||
activity.accountId === accountId &&
|
||||
activity.SymbolProfile.currency === currency &&
|
||||
activity.SymbolProfile.dataSource === dataSource &&
|
||||
isSameDay(activity.date, date) &&
|
||||
|
@ -13,7 +13,8 @@ import {
|
||||
IsISO8601,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString
|
||||
IsString,
|
||||
Min
|
||||
} from 'class-validator';
|
||||
import { isString } from 'lodash';
|
||||
|
||||
@ -48,9 +49,11 @@ export class CreateOrderDto {
|
||||
date: string;
|
||||
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
fee: number;
|
||||
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
quantity: number;
|
||||
|
||||
@IsString()
|
||||
@ -64,6 +67,7 @@ export class CreateOrderDto {
|
||||
type: Type;
|
||||
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
unitPrice: number;
|
||||
|
||||
@IsBoolean()
|
||||
|
@ -8,12 +8,12 @@ import {
|
||||
import { Transform, TransformFnParams } from 'class-transformer';
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsISO8601,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString
|
||||
IsString,
|
||||
Min
|
||||
} from 'class-validator';
|
||||
import { isString } from 'lodash';
|
||||
|
||||
@ -47,12 +47,14 @@ export class UpdateOrderDto {
|
||||
date: string;
|
||||
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
fee: number;
|
||||
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
quantity: number;
|
||||
|
||||
@IsString()
|
||||
@ -66,5 +68,6 @@ export class UpdateOrderDto {
|
||||
type: Type;
|
||||
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
unitPrice: number;
|
||||
}
|
||||
|
@ -323,7 +323,8 @@ export class PortfolioController {
|
||||
@Query('accounts') filterByAccounts?: string,
|
||||
@Query('assetClasses') filterByAssetClasses?: string,
|
||||
@Query('range') dateRange: DateRange = 'max',
|
||||
@Query('tags') filterByTags?: string
|
||||
@Query('tags') filterByTags?: string,
|
||||
@Query('withExcludedAccounts') withExcludedAccounts = false
|
||||
): Promise<PortfolioPerformanceResponse> {
|
||||
const filters = this.apiService.buildFiltersFromQueryParams({
|
||||
filterByAccounts,
|
||||
@ -335,6 +336,7 @@ export class PortfolioController {
|
||||
dateRange,
|
||||
filters,
|
||||
impersonationId,
|
||||
withExcludedAccounts,
|
||||
userId: this.request.user.id
|
||||
});
|
||||
|
||||
|
@ -372,20 +372,23 @@ export class PortfolioService {
|
||||
filters,
|
||||
impersonationId,
|
||||
userCurrency,
|
||||
userId
|
||||
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
|
||||
userId,
|
||||
withExcludedAccounts
|
||||
});
|
||||
|
||||
const portfolioCalculator = new PortfolioCalculator({
|
||||
@ -1110,12 +1113,14 @@ export class PortfolioService {
|
||||
dateRange = 'max',
|
||||
filters,
|
||||
impersonationId,
|
||||
userId
|
||||
userId,
|
||||
withExcludedAccounts = false
|
||||
}: {
|
||||
dateRange?: DateRange;
|
||||
filters?: Filter[];
|
||||
impersonationId: string;
|
||||
userId: string;
|
||||
withExcludedAccounts?: boolean;
|
||||
}): Promise<PortfolioPerformanceResponse> {
|
||||
userId = await this.getUserId(impersonationId, userId);
|
||||
const user = await this.userService.user({ id: userId });
|
||||
@ -1124,7 +1129,8 @@ export class PortfolioService {
|
||||
const { portfolioOrders, transactionPoints } =
|
||||
await this.getTransactionPoints({
|
||||
filters,
|
||||
userId
|
||||
userId,
|
||||
withExcludedAccounts
|
||||
});
|
||||
|
||||
const portfolioCalculator = new PortfolioCalculator({
|
||||
@ -1174,7 +1180,8 @@ export class PortfolioService {
|
||||
filters,
|
||||
impersonationId,
|
||||
userCurrency,
|
||||
userId
|
||||
userId,
|
||||
withExcludedAccounts
|
||||
});
|
||||
|
||||
const itemOfToday = historicalDataContainer.items.find((item) => {
|
||||
@ -1763,7 +1770,7 @@ export class PortfolioService {
|
||||
filters,
|
||||
includeDrafts = false,
|
||||
userId,
|
||||
withExcludedAccounts
|
||||
withExcludedAccounts = false
|
||||
}: {
|
||||
filters?: Filter[];
|
||||
includeDrafts?: boolean;
|
||||
@ -1851,7 +1858,7 @@ export class PortfolioService {
|
||||
portfolioItemsNow,
|
||||
userCurrency,
|
||||
userId,
|
||||
withExcludedAccounts
|
||||
withExcludedAccounts = false
|
||||
}: {
|
||||
filters?: Filter[];
|
||||
orders: OrderWithAccount[];
|
||||
@ -1885,9 +1892,13 @@ export class PortfolioService {
|
||||
});
|
||||
} else {
|
||||
const accountIds = uniq(
|
||||
orders.map(({ accountId }) => {
|
||||
return accountId;
|
||||
})
|
||||
orders
|
||||
.filter(({ accountId }) => {
|
||||
return accountId;
|
||||
})
|
||||
.map(({ accountId }) => {
|
||||
return accountId;
|
||||
})
|
||||
);
|
||||
|
||||
currentAccounts = await this.accountService.accounts({
|
||||
|
@ -54,10 +54,22 @@
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-8figures</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-altoo</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-beanvest</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-capitally</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-capmon</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -82,6 +94,10 @@
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-finary</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-finwise</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-folishare</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -94,6 +110,10 @@
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-gospatz</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-intuit-mint</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-justetf</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -134,6 +154,10 @@
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-projectionlab</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-rocket-money</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-seeking-alpha</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -166,6 +190,14 @@
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-utluna</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-vyzer</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-wealthica</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-yeekatee</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -274,6 +306,10 @@
|
||||
<loc>https://ghostfol.io/en/blog/2023/09/hacktoberfest-2023</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/blog/2023/11/hacktoberfest-2023-debriefing</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/faq</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -308,10 +344,22 @@
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-8figures</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-altoo</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-beanvest</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-capitally</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-capmon</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -336,6 +384,10 @@
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-finary</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-finwise</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-folishare</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -348,6 +400,10 @@
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-gospatz</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-intuit-mint</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-justetf</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -388,6 +444,10 @@
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-projectionlab</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-rocket-money</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-seeking-alpha</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -420,6 +480,14 @@
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-utluna</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-vyzer</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-wealthica</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-yeekatee</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -590,12 +658,24 @@
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-8figures</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-altoo</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-campmon</loc>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-beanvest</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-capitally</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-capmon</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
@ -618,6 +698,10 @@
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-finary</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-finwise</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-folishare</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -630,6 +714,10 @@
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-gospatz</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-intuit-mint</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-justetf</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -670,6 +758,10 @@
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-projectionlab</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-rocket-money</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-seeking-alpha</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -702,6 +794,14 @@
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-utluna</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-vyzer</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-wealthica</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-yeekatee</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -718,10 +818,22 @@
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-8figures</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-altoo</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-beanvest</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-capitally</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-capmon</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -746,6 +858,10 @@
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-finary</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-finwise</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-folishare</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -758,6 +874,10 @@
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-gospatz</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-intuit-mint</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-justetf</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -798,6 +918,10 @@
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-projectionlab</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-rocket-money</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-seeking-alpha</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -830,6 +954,14 @@
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-utluna</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-vyzer</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-wealthica</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-yeekatee</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
|
@ -12,13 +12,12 @@ import { DATE_FORMAT, interpolate } from '@ghostfolio/common/helper';
|
||||
import { format } from 'date-fns';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
|
||||
const title = 'Ghostfolio – Open Source Wealth Management Software';
|
||||
const titleShort = 'Ghostfolio';
|
||||
|
||||
const i18nService = new I18nService();
|
||||
|
||||
let indexHtmlMap: { [languageCode: string]: string } = {};
|
||||
|
||||
const title = 'Ghostfolio';
|
||||
|
||||
try {
|
||||
indexHtmlMap = SUPPORTED_LANGUAGE_CODES.reduce(
|
||||
(map, languageCode) => ({
|
||||
@ -35,47 +34,51 @@ try {
|
||||
const locales = {
|
||||
'/de/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt': {
|
||||
featureGraphicPath: 'assets/images/blog/ghostfolio-x-sackgeld.png',
|
||||
title: `Ghostfolio auf Sackgeld.com vorgestellt - ${titleShort}`
|
||||
title: `Ghostfolio auf Sackgeld.com vorgestellt - ${title}`
|
||||
},
|
||||
'/en/blog/2022/08/500-stars-on-github': {
|
||||
featureGraphicPath: 'assets/images/blog/500-stars-on-github.jpg',
|
||||
title: `500 Stars - ${titleShort}`
|
||||
title: `500 Stars - ${title}`
|
||||
},
|
||||
'/en/blog/2022/10/hacktoberfest-2022': {
|
||||
featureGraphicPath: 'assets/images/blog/hacktoberfest-2022.png',
|
||||
title: `Hacktoberfest 2022 - ${titleShort}`
|
||||
title: `Hacktoberfest 2022 - ${title}`
|
||||
},
|
||||
'/en/blog/2022/12/the-importance-of-tracking-your-personal-finances': {
|
||||
featureGraphicPath: 'assets/images/blog/20221226.jpg',
|
||||
title: `The importance of tracking your personal finances - ${titleShort}`
|
||||
title: `The importance of tracking your personal finances - ${title}`
|
||||
},
|
||||
'/en/blog/2023/02/ghostfolio-meets-umbrel': {
|
||||
featureGraphicPath: 'assets/images/blog/ghostfolio-x-umbrel.png',
|
||||
title: `Ghostfolio meets Umbrel - ${titleShort}`
|
||||
title: `Ghostfolio meets Umbrel - ${title}`
|
||||
},
|
||||
'/en/blog/2023/03/ghostfolio-reaches-1000-stars-on-github': {
|
||||
featureGraphicPath: 'assets/images/blog/1000-stars-on-github.jpg',
|
||||
title: `Ghostfolio reaches 1’000 Stars on GitHub - ${titleShort}`
|
||||
title: `Ghostfolio reaches 1’000 Stars on GitHub - ${title}`
|
||||
},
|
||||
'/en/blog/2023/05/unlock-your-financial-potential-with-ghostfolio': {
|
||||
featureGraphicPath: 'assets/images/blog/20230520.jpg',
|
||||
title: `Unlock your Financial Potential with Ghostfolio - ${titleShort}`
|
||||
title: `Unlock your Financial Potential with Ghostfolio - ${title}`
|
||||
},
|
||||
'/en/blog/2023/07/exploring-the-path-to-fire': {
|
||||
featureGraphicPath: 'assets/images/blog/20230701.jpg',
|
||||
title: `Exploring the Path to FIRE - ${titleShort}`
|
||||
title: `Exploring the Path to FIRE - ${title}`
|
||||
},
|
||||
'/en/blog/2023/08/ghostfolio-joins-oss-friends': {
|
||||
featureGraphicPath: 'assets/images/blog/ghostfolio-joins-oss-friends.png',
|
||||
title: `Ghostfolio joins OSS Friends - ${titleShort}`
|
||||
title: `Ghostfolio joins OSS Friends - ${title}`
|
||||
},
|
||||
'/en/blog/2023/09/ghostfolio-2': {
|
||||
featureGraphicPath: 'assets/images/blog/ghostfolio-2.jpg',
|
||||
title: `Announcing Ghostfolio 2.0 - ${titleShort}`
|
||||
title: `Announcing Ghostfolio 2.0 - ${title}`
|
||||
},
|
||||
'/en/blog/2023/09/hacktoberfest-2023': {
|
||||
featureGraphicPath: 'assets/images/blog/hacktoberfest-2023.png',
|
||||
title: `Hacktoberfest 2023 - ${titleShort}`
|
||||
title: `Hacktoberfest 2023 - ${title}`
|
||||
},
|
||||
'/en/blog/2023/11/hacktoberfest-2023-debriefing': {
|
||||
featureGraphicPath: 'assets/images/blog/hacktoberfest-2023.png',
|
||||
title: `Hacktoberfest 2023 Debriefing - ${title}`
|
||||
}
|
||||
};
|
||||
|
||||
@ -128,7 +131,16 @@ export const HtmlTemplateMiddleware = async (
|
||||
}),
|
||||
featureGraphicPath:
|
||||
locales[path]?.featureGraphicPath ?? 'assets/cover.png',
|
||||
title: locales[path]?.title ?? title
|
||||
keywords: i18nService.getTranslation({
|
||||
languageCode,
|
||||
id: 'metaKeywords'
|
||||
}),
|
||||
title:
|
||||
locales[path]?.title ??
|
||||
`${title} – ${i18nService.getTranslation({
|
||||
languageCode,
|
||||
id: 'slogan'
|
||||
})}`
|
||||
});
|
||||
|
||||
return response.send(indexHtml);
|
||||
|
@ -105,9 +105,11 @@ export class AlphaVantageService implements DataProviderInterface {
|
||||
return DataSource.ALPHA_VANTAGE;
|
||||
}
|
||||
|
||||
public async getQuotes(
|
||||
aSymbols: string[]
|
||||
): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
||||
public async getQuotes({
|
||||
symbols
|
||||
}: {
|
||||
symbols: string[];
|
||||
}): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
||||
return {};
|
||||
}
|
||||
|
||||
|
@ -134,13 +134,15 @@ export class CoinGeckoService implements DataProviderInterface {
|
||||
return DataSource.COINGECKO;
|
||||
}
|
||||
|
||||
public async getQuotes(
|
||||
aSymbols: string[]
|
||||
): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
||||
const results: { [symbol: string]: IDataProviderResponse } = {};
|
||||
public async getQuotes({
|
||||
symbols
|
||||
}: {
|
||||
symbols: string[];
|
||||
}): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
||||
const response: { [symbol: string]: IDataProviderResponse } = {};
|
||||
|
||||
if (aSymbols.length <= 0) {
|
||||
return {};
|
||||
if (symbols.length <= 0) {
|
||||
return response;
|
||||
}
|
||||
|
||||
try {
|
||||
@ -150,8 +152,8 @@ export class CoinGeckoService implements DataProviderInterface {
|
||||
abortController.abort();
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const response = await got(
|
||||
`${this.URL}/simple/price?ids=${aSymbols.join(
|
||||
const quotes = await got(
|
||||
`${this.URL}/simple/price?ids=${symbols.join(
|
||||
','
|
||||
)}&vs_currencies=${DEFAULT_CURRENCY.toLowerCase()}`,
|
||||
{
|
||||
@ -160,22 +162,20 @@ export class CoinGeckoService implements DataProviderInterface {
|
||||
}
|
||||
).json<any>();
|
||||
|
||||
for (const symbol in response) {
|
||||
if (Object.prototype.hasOwnProperty.call(response, symbol)) {
|
||||
results[symbol] = {
|
||||
currency: DEFAULT_CURRENCY,
|
||||
dataProviderInfo: this.getDataProviderInfo(),
|
||||
dataSource: DataSource.COINGECKO,
|
||||
marketPrice: response[symbol][DEFAULT_CURRENCY.toLowerCase()],
|
||||
marketState: 'open'
|
||||
};
|
||||
}
|
||||
for (const symbol in quotes) {
|
||||
response[symbol] = {
|
||||
currency: DEFAULT_CURRENCY,
|
||||
dataProviderInfo: this.getDataProviderInfo(),
|
||||
dataSource: DataSource.COINGECKO,
|
||||
marketPrice: quotes[symbol][DEFAULT_CURRENCY.toLowerCase()],
|
||||
marketState: 'open'
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(error, 'CoinGeckoService');
|
||||
}
|
||||
|
||||
return results;
|
||||
return response;
|
||||
}
|
||||
|
||||
public getTestSymbol() {
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
Prisma,
|
||||
SymbolProfile
|
||||
} from '@prisma/client';
|
||||
import { isISIN } from 'class-validator';
|
||||
import { countries } from 'countries-list';
|
||||
import yahooFinance from 'yahoo-finance2';
|
||||
import type { Price } from 'yahoo-finance2/dist/esm/src/modules/quoteSummary-iface';
|
||||
@ -156,7 +157,20 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
|
||||
const response: Partial<SymbolProfile> = {};
|
||||
|
||||
try {
|
||||
const symbol = this.convertToYahooFinanceSymbol(aSymbol);
|
||||
let symbol = aSymbol;
|
||||
|
||||
if (isISIN(symbol)) {
|
||||
try {
|
||||
const { quotes } = await yahooFinance.search(symbol);
|
||||
|
||||
if (quotes.length === 1) {
|
||||
symbol = quotes[0].symbol;
|
||||
}
|
||||
} catch {}
|
||||
} else {
|
||||
symbol = this.convertToYahooFinanceSymbol(symbol);
|
||||
}
|
||||
|
||||
const assetProfile = await yahooFinance.quoteSummary(symbol, {
|
||||
modules: ['price', 'summaryProfile', 'topHoldings']
|
||||
});
|
||||
@ -176,7 +190,7 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
|
||||
shortName: assetProfile.price.shortName,
|
||||
symbol: assetProfile.price.symbol
|
||||
});
|
||||
response.symbol = aSymbol;
|
||||
response.symbol = assetProfile.price.symbol;
|
||||
|
||||
if (assetSubClass === AssetSubClass.MUTUALFUND) {
|
||||
response.sectors = [];
|
||||
|
@ -311,7 +311,9 @@ export class DataProviderService {
|
||||
i + maximumNumberOfSymbolsPerRequest
|
||||
);
|
||||
|
||||
const promise = Promise.resolve(dataProvider.getQuotes(symbolsChunk));
|
||||
const promise = Promise.resolve(
|
||||
dataProvider.getQuotes({ symbols: symbolsChunk })
|
||||
);
|
||||
|
||||
promises.push(
|
||||
promise.then(async (result) => {
|
||||
|
@ -131,17 +131,21 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
return DataSource.EOD_HISTORICAL_DATA;
|
||||
}
|
||||
|
||||
public async getQuotes(
|
||||
aSymbols: string[]
|
||||
): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
||||
const symbols = aSymbols.map((symbol) => {
|
||||
return this.convertToEodSymbol(symbol);
|
||||
});
|
||||
public async getQuotes({
|
||||
symbols
|
||||
}: {
|
||||
symbols: string[];
|
||||
}): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
||||
let response: { [symbol: string]: IDataProviderResponse } = {};
|
||||
|
||||
if (symbols.length <= 0) {
|
||||
return {};
|
||||
return response;
|
||||
}
|
||||
|
||||
const eodHistoricalDataSymbols = symbols.map((symbol) => {
|
||||
return this.convertToEodSymbol(symbol);
|
||||
});
|
||||
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
|
||||
@ -150,9 +154,9 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const realTimeResponse = await got(
|
||||
`${this.URL}/real-time/${symbols[0]}?api_token=${
|
||||
`${this.URL}/real-time/${eodHistoricalDataSymbols[0]}?api_token=${
|
||||
this.apiKey
|
||||
}&fmt=json&s=${symbols.join(',')}`,
|
||||
}&fmt=json&s=${eodHistoricalDataSymbols.join(',')}`,
|
||||
{
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
@ -160,10 +164,12 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
).json<any>();
|
||||
|
||||
const quotes =
|
||||
symbols.length === 1 ? [realTimeResponse] : realTimeResponse;
|
||||
eodHistoricalDataSymbols.length === 1
|
||||
? [realTimeResponse]
|
||||
: realTimeResponse;
|
||||
|
||||
const searchResponse = await Promise.all(
|
||||
symbols
|
||||
eodHistoricalDataSymbols
|
||||
.filter((symbol) => {
|
||||
return !symbol.endsWith('.FOREX');
|
||||
})
|
||||
@ -176,7 +182,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
return items[0];
|
||||
});
|
||||
|
||||
const response = quotes.reduce(
|
||||
response = quotes.reduce(
|
||||
(
|
||||
result: { [symbol: string]: IDataProviderResponse },
|
||||
{ close, code, timestamp }
|
||||
|
@ -113,13 +113,15 @@ export class FinancialModelingPrepService implements DataProviderInterface {
|
||||
return DataSource.FINANCIAL_MODELING_PREP;
|
||||
}
|
||||
|
||||
public async getQuotes(
|
||||
aSymbols: string[]
|
||||
): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
||||
const results: { [symbol: string]: IDataProviderResponse } = {};
|
||||
public async getQuotes({
|
||||
symbols
|
||||
}: {
|
||||
symbols: string[];
|
||||
}): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
||||
const response: { [symbol: string]: IDataProviderResponse } = {};
|
||||
|
||||
if (aSymbols.length <= 0) {
|
||||
return {};
|
||||
if (symbols.length <= 0) {
|
||||
return response;
|
||||
}
|
||||
|
||||
try {
|
||||
@ -130,7 +132,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
|
||||
}, DEFAULT_REQUEST_TIMEOUT);
|
||||
|
||||
const response = await got(
|
||||
`${this.URL}/quote/${aSymbols.join(',')}?apikey=${this.apiKey}`,
|
||||
`${this.URL}/quote/${symbols.join(',')}?apikey=${this.apiKey}`,
|
||||
{
|
||||
// @ts-ignore
|
||||
signal: abortController.signal
|
||||
@ -138,7 +140,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
|
||||
).json<any>();
|
||||
|
||||
for (const { price, symbol } of response) {
|
||||
results[symbol] = {
|
||||
response[symbol] = {
|
||||
currency: DEFAULT_CURRENCY,
|
||||
dataProviderInfo: this.getDataProviderInfo(),
|
||||
dataSource: DataSource.FINANCIAL_MODELING_PREP,
|
||||
@ -150,7 +152,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
|
||||
Logger.error(error, 'FinancialModelingPrepService');
|
||||
}
|
||||
|
||||
return results;
|
||||
return response;
|
||||
}
|
||||
|
||||
public getTestSymbol() {
|
||||
|
@ -99,18 +99,20 @@ export class GoogleSheetsService implements DataProviderInterface {
|
||||
return DataSource.GOOGLE_SHEETS;
|
||||
}
|
||||
|
||||
public async getQuotes(
|
||||
aSymbols: string[]
|
||||
): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
||||
if (aSymbols.length <= 0) {
|
||||
return {};
|
||||
public async getQuotes({
|
||||
symbols
|
||||
}: {
|
||||
symbols: string[];
|
||||
}): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
||||
const response: { [symbol: string]: IDataProviderResponse } = {};
|
||||
|
||||
if (symbols.length <= 0) {
|
||||
return response;
|
||||
}
|
||||
|
||||
try {
|
||||
const response: { [symbol: string]: IDataProviderResponse } = {};
|
||||
|
||||
const symbolProfiles = await this.symbolProfileService.getSymbolProfiles(
|
||||
aSymbols.map((symbol) => {
|
||||
symbols.map((symbol) => {
|
||||
return {
|
||||
symbol,
|
||||
dataSource: this.getName()
|
||||
@ -129,7 +131,7 @@ export class GoogleSheetsService implements DataProviderInterface {
|
||||
const marketPrice = parseFloat(row['marketPrice']);
|
||||
const symbol = row['symbol'];
|
||||
|
||||
if (aSymbols.includes(symbol)) {
|
||||
if (symbols.includes(symbol)) {
|
||||
response[symbol] = {
|
||||
marketPrice,
|
||||
currency: symbolProfiles.find((symbolProfile) => {
|
||||
|
@ -36,9 +36,11 @@ export interface DataProviderInterface {
|
||||
|
||||
getName(): DataSource;
|
||||
|
||||
getQuotes(
|
||||
aSymbols: string[]
|
||||
): Promise<{ [symbol: string]: IDataProviderResponse }>;
|
||||
getQuotes({
|
||||
symbols
|
||||
}: {
|
||||
symbols: string[];
|
||||
}): Promise<{ [symbol: string]: IDataProviderResponse }>;
|
||||
|
||||
getTestSymbol(): string;
|
||||
|
||||
|
@ -133,18 +133,20 @@ export class ManualService implements DataProviderInterface {
|
||||
return DataSource.MANUAL;
|
||||
}
|
||||
|
||||
public async getQuotes(
|
||||
aSymbols: string[]
|
||||
): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
||||
public async getQuotes({
|
||||
symbols
|
||||
}: {
|
||||
symbols: string[];
|
||||
}): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
||||
const response: { [symbol: string]: IDataProviderResponse } = {};
|
||||
|
||||
if (aSymbols.length <= 0) {
|
||||
if (symbols.length <= 0) {
|
||||
return response;
|
||||
}
|
||||
|
||||
try {
|
||||
const symbolProfiles = await this.symbolProfileService.getSymbolProfiles(
|
||||
aSymbols.map((symbol) => {
|
||||
symbols.map((symbol) => {
|
||||
return { symbol, dataSource: this.getName() };
|
||||
})
|
||||
);
|
||||
@ -154,10 +156,10 @@ export class ManualService implements DataProviderInterface {
|
||||
orderBy: {
|
||||
date: 'desc'
|
||||
},
|
||||
take: aSymbols.length,
|
||||
take: symbols.length,
|
||||
where: {
|
||||
symbol: {
|
||||
in: aSymbols
|
||||
in: symbols
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -87,15 +87,17 @@ export class RapidApiService implements DataProviderInterface {
|
||||
return DataSource.RAPID_API;
|
||||
}
|
||||
|
||||
public async getQuotes(
|
||||
aSymbols: string[]
|
||||
): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
||||
if (aSymbols.length <= 0) {
|
||||
public async getQuotes({
|
||||
symbols
|
||||
}: {
|
||||
symbols: string[];
|
||||
}): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
||||
if (symbols.length <= 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
const symbol = aSymbols[0];
|
||||
const symbol = symbols[0];
|
||||
|
||||
if (symbol === ghostfolioFearAndGreedIndexSymbol) {
|
||||
const fgi = await this.getFearAndGreedIndex();
|
||||
|
@ -30,7 +30,7 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
public async getAssetProfile(
|
||||
aSymbol: string
|
||||
): Promise<Partial<SymbolProfile>> {
|
||||
const { assetClass, assetSubClass, currency, name } =
|
||||
const { assetClass, assetSubClass, currency, name, symbol } =
|
||||
await this.yahooFinanceDataEnhancerService.getAssetProfile(aSymbol);
|
||||
|
||||
return {
|
||||
@ -38,8 +38,8 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
assetSubClass,
|
||||
currency,
|
||||
name,
|
||||
dataSource: this.getName(),
|
||||
symbol: aSymbol
|
||||
symbol,
|
||||
dataSource: this.getName()
|
||||
};
|
||||
}
|
||||
|
||||
@ -156,20 +156,22 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
return DataSource.YAHOO;
|
||||
}
|
||||
|
||||
public async getQuotes(
|
||||
aSymbols: string[]
|
||||
): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
||||
if (aSymbols.length <= 0) {
|
||||
return {};
|
||||
public async getQuotes({
|
||||
symbols
|
||||
}: {
|
||||
symbols: string[];
|
||||
}): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
||||
const response: { [symbol: string]: IDataProviderResponse } = {};
|
||||
|
||||
if (symbols.length <= 0) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const yahooFinanceSymbols = aSymbols.map((symbol) =>
|
||||
const yahooFinanceSymbols = symbols.map((symbol) =>
|
||||
this.yahooFinanceDataEnhancerService.convertToYahooFinanceSymbol(symbol)
|
||||
);
|
||||
|
||||
try {
|
||||
const response: { [symbol: string]: IDataProviderResponse } = {};
|
||||
|
||||
let quotes: Pick<
|
||||
Quote,
|
||||
'currency' | 'marketState' | 'regularMarketPrice' | 'symbol'
|
||||
|
@ -95,6 +95,30 @@ export class ExchangeRateDataService {
|
||||
const [currency1, currency2] = symbol.match(/.{1,3}/g);
|
||||
const [date] = Object.keys(result[symbol]);
|
||||
|
||||
// Add derived currencies
|
||||
if (currency2 === 'GBP') {
|
||||
resultExtended[`${currency1}GBp`] = {
|
||||
[date]: {
|
||||
marketPrice:
|
||||
result[`${currency1}${currency2}`][date].marketPrice * 100
|
||||
}
|
||||
};
|
||||
} else if (currency2 === 'ILS') {
|
||||
resultExtended[`${currency1}ILA`] = {
|
||||
[date]: {
|
||||
marketPrice:
|
||||
result[`${currency1}${currency2}`][date].marketPrice * 100
|
||||
}
|
||||
};
|
||||
} else if (currency2 === 'ZAR') {
|
||||
resultExtended[`${currency1}ZAc`] = {
|
||||
[date]: {
|
||||
marketPrice:
|
||||
result[`${currency1}${currency2}`][date].marketPrice * 100
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Calculate the opposite direction
|
||||
resultExtended[`${currency2}${currency1}`] = {
|
||||
[date]: {
|
||||
|
@ -86,14 +86,24 @@ export class SymbolProfileService {
|
||||
}
|
||||
|
||||
public updateSymbolProfile({
|
||||
assetClass,
|
||||
assetSubClass,
|
||||
comment,
|
||||
dataSource,
|
||||
name,
|
||||
scraperConfiguration,
|
||||
symbol,
|
||||
symbolMapping
|
||||
}: Prisma.SymbolProfileUpdateInput & UniqueAsset) {
|
||||
return this.prismaService.symbolProfile.update({
|
||||
data: { comment, scraperConfiguration, symbolMapping },
|
||||
data: {
|
||||
assetClass,
|
||||
assetSubClass,
|
||||
comment,
|
||||
name,
|
||||
scraperConfiguration,
|
||||
symbolMapping
|
||||
},
|
||||
where: { dataSource_symbol: { dataSource, symbol } }
|
||||
});
|
||||
}
|
||||
|
@ -116,7 +116,8 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
||||
type: 'ACCOUNT'
|
||||
}
|
||||
],
|
||||
range: 'max'
|
||||
range: 'max',
|
||||
withExcludedAccounts: true
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ chart }) => {
|
||||
|
@ -2,6 +2,7 @@
|
||||
<button
|
||||
class="align-items-center d-flex"
|
||||
mat-stroked-button
|
||||
[disabled]="dataSource?.data.length < 2"
|
||||
(click)="onTransferBalance()"
|
||||
>
|
||||
<ion-icon class="mr-2" name="arrow-redo-outline"></ion-icon>
|
||||
@ -253,16 +254,20 @@
|
||||
</button>
|
||||
<mat-menu #accountMenu="matMenu" xPosition="before">
|
||||
<button mat-menu-item (click)="onUpdateAccount(element)">
|
||||
<ion-icon class="mr-2" name="create-outline"></ion-icon>
|
||||
<span i18n>Edit</span>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="create-outline"></ion-icon>
|
||||
<span i18n>Edit</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
[disabled]="element.isDefault || element.transactionCount > 0"
|
||||
(click)="onDeleteAccount(element.id)"
|
||||
>
|
||||
<ion-icon class="mr-2" name="trash-outline"></ion-icon>
|
||||
<span i18n>Delete</span>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="trash-outline"></ion-icon>
|
||||
<span i18n>Delete</span>
|
||||
</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
|
@ -57,10 +57,16 @@ export class MarketDataDetailDialog implements OnDestroy {
|
||||
|
||||
public onUpdate() {
|
||||
this.adminService
|
||||
.putMarketData({
|
||||
.postMarketData({
|
||||
dataSource: this.data.dataSource,
|
||||
date: this.data.date,
|
||||
marketData: { marketPrice: this.data.marketPrice },
|
||||
marketData: {
|
||||
marketData: [
|
||||
{
|
||||
date: this.data.date.toISOString(),
|
||||
marketPrice: this.data.marketPrice
|
||||
}
|
||||
]
|
||||
},
|
||||
symbol: this.data.symbol
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
|
@ -143,12 +143,24 @@
|
||||
<ion-icon name="ellipsis-horizontal"></ion-icon>
|
||||
</button>
|
||||
<mat-menu #assetProfileActionsMenu="matMenu" xPosition="before">
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="onOpenAssetProfileDialog({ dataSource: element.dataSource, symbol: element.symbol })"
|
||||
>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="create-outline"></ion-icon>
|
||||
<span i18n>Edit</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
[disabled]="element.activitiesCount !== 0"
|
||||
(click)="onDeleteProfileData({dataSource: element.dataSource, symbol: element.symbol})"
|
||||
>
|
||||
<ng-container i18n>Delete</ng-container>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="trash-outline"></ion-icon>
|
||||
<span i18n>Delete</span>
|
||||
</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
|
@ -6,19 +6,24 @@ import {
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { FormBuilder } from '@angular/forms';
|
||||
import { FormBuilder, FormControl, Validators } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto';
|
||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
AdminMarketDataDetails,
|
||||
UniqueAsset
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { translate } from '@ghostfolio/ui/i18n';
|
||||
import { MarketData, SymbolProfile } from '@prisma/client';
|
||||
import { format, parseISO } from 'date-fns';
|
||||
import {
|
||||
AssetClass,
|
||||
AssetSubClass,
|
||||
MarketData,
|
||||
SymbolProfile
|
||||
} from '@prisma/client';
|
||||
import { format } from 'date-fns';
|
||||
import { parse as csvToJson } from 'papaparse';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
@ -33,14 +38,23 @@ import { AssetProfileDialogParams } from './interfaces/interfaces';
|
||||
styleUrls: ['./asset-profile-dialog.component.scss']
|
||||
})
|
||||
export class AssetProfileDialog implements OnDestroy, OnInit {
|
||||
public assetClass: string;
|
||||
public assetProfileClass: string;
|
||||
public assetClasses = Object.keys(AssetClass).map((assetClass) => {
|
||||
return { id: assetClass, label: translate(assetClass) };
|
||||
});
|
||||
public assetSubClasses = Object.keys(AssetSubClass).map((assetSubClass) => {
|
||||
return { id: assetSubClass, label: translate(assetSubClass) };
|
||||
});
|
||||
public assetProfile: AdminMarketDataDetails['assetProfile'];
|
||||
public assetProfileForm = this.formBuilder.group({
|
||||
assetClass: new FormControl<AssetClass>(undefined),
|
||||
assetSubClass: new FormControl<AssetSubClass>(undefined),
|
||||
comment: '',
|
||||
name: ['', Validators.required],
|
||||
scraperConfiguration: '',
|
||||
symbolMapping: ''
|
||||
});
|
||||
public assetSubClass: string;
|
||||
public assetProfileSubClass: string;
|
||||
public benchmarks: Partial<SymbolProfile>[];
|
||||
public countries: {
|
||||
[code: string]: { name: string; value: number };
|
||||
@ -86,8 +100,8 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
||||
.subscribe(({ assetProfile, marketData }) => {
|
||||
this.assetProfile = assetProfile;
|
||||
|
||||
this.assetClass = translate(this.assetProfile?.assetClass);
|
||||
this.assetSubClass = translate(this.assetProfile?.assetSubClass);
|
||||
this.assetProfileClass = translate(this.assetProfile?.assetClass);
|
||||
this.assetProfileSubClass = translate(this.assetProfile?.assetSubClass);
|
||||
this.countries = {};
|
||||
this.isBenchmark = this.benchmarks.some(({ id }) => {
|
||||
return id === this.assetProfile.id;
|
||||
@ -114,7 +128,10 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
this.assetProfileForm.setValue({
|
||||
assetClass: this.assetProfile.assetClass ?? null,
|
||||
assetSubClass: this.assetProfile.assetSubClass ?? null,
|
||||
comment: this.assetProfile?.comment ?? '',
|
||||
name: this.assetProfile.name ?? this.assetProfile.symbol,
|
||||
scraperConfiguration: JSON.stringify(
|
||||
this.assetProfile?.scraperConfiguration ?? {}
|
||||
),
|
||||
@ -157,7 +174,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
||||
dataSource: this.data.dataSource,
|
||||
marketData: {
|
||||
marketData: marketData.map(({ date, marketPrice }) => {
|
||||
return { marketPrice, date: parseISO(date) };
|
||||
return { marketPrice, date: parseDate(date).toISOString() };
|
||||
})
|
||||
},
|
||||
symbol: this.data.symbol
|
||||
@ -204,9 +221,12 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
||||
} catch {}
|
||||
|
||||
const assetProfileData: UpdateAssetProfileDto = {
|
||||
assetClass: this.assetProfileForm.controls['assetClass'].value,
|
||||
assetSubClass: this.assetProfileForm.controls['assetSubClass'].value,
|
||||
comment: this.assetProfileForm.controls['comment'].value ?? null,
|
||||
name: this.assetProfileForm.controls['name'].value,
|
||||
scraperConfiguration,
|
||||
symbolMapping,
|
||||
comment: this.assetProfileForm.controls['comment'].value ?? null
|
||||
symbolMapping
|
||||
};
|
||||
|
||||
this.adminService
|
||||
|
@ -112,7 +112,11 @@
|
||||
>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<gf-value i18n size="medium" [hidden]="!assetClass" [value]="assetClass"
|
||||
<gf-value
|
||||
i18n
|
||||
size="medium"
|
||||
[hidden]="!assetProfileClass"
|
||||
[value]="assetProfileClass"
|
||||
>Asset Class</gf-value
|
||||
>
|
||||
</div>
|
||||
@ -120,8 +124,8 @@
|
||||
<gf-value
|
||||
i18n
|
||||
size="medium"
|
||||
[hidden]="!assetSubClass"
|
||||
[value]="assetSubClass"
|
||||
[hidden]="!assetProfileSubClass"
|
||||
[value]="assetProfileSubClass"
|
||||
>Asset Sub Class</gf-value
|
||||
>
|
||||
</div>
|
||||
@ -174,6 +178,38 @@
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div *ngIf="assetProfile?.dataSource === 'MANUAL'" class="mt-3">
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Name</mat-label>
|
||||
<input formControlName="name" matInput type="text" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div *ngIf="assetProfile?.dataSource === 'MANUAL'" class="mt-3">
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Asset Class</mat-label>
|
||||
<mat-select formControlName="assetClass">
|
||||
<mat-option [value]="null"></mat-option>
|
||||
<mat-option
|
||||
*ngFor="let assetClass of assetClasses"
|
||||
[value]="assetClass.id"
|
||||
>{{ assetClass.label }}</mat-option
|
||||
>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div *ngIf="assetProfile?.dataSource === 'MANUAL'" class="mt-3">
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Asset Sub Class</mat-label>
|
||||
<mat-select formControlName="assetSubClass">
|
||||
<mat-option [value]="null"></mat-option>
|
||||
<mat-option
|
||||
*ngFor="let assetSubClass of assetSubClasses"
|
||||
[value]="assetSubClass.id"
|
||||
>{{ assetSubClass.label }}</mat-option
|
||||
>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="d-flex my-3">
|
||||
<div class="w-50">
|
||||
<mat-checkbox
|
||||
|
@ -7,6 +7,7 @@ import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { GfAdminMarketDataDetailModule } from '@ghostfolio/client/components/admin-market-data-detail/admin-market-data-detail.module';
|
||||
import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module';
|
||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||
@ -26,6 +27,7 @@ import { AssetProfileDialog } from './asset-profile-dialog.component';
|
||||
MatDialogModule,
|
||||
MatInputModule,
|
||||
MatMenuModule,
|
||||
MatSelectModule,
|
||||
ReactiveFormsModule,
|
||||
TextFieldModule
|
||||
],
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { MatCheckboxChange } from '@angular/material/checkbox';
|
||||
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
|
||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||
import { CacheService } from '@ghostfolio/client/services/cache.service';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
@ -169,20 +169,20 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
public onReadOnlyModeChange(aEvent: MatCheckboxChange) {
|
||||
this.putAdminSetting({
|
||||
key: PROPERTY_IS_READ_ONLY_MODE,
|
||||
value: aEvent.checked ? true : undefined
|
||||
});
|
||||
}
|
||||
|
||||
public onEnableUserSignupModeChange(aEvent: MatCheckboxChange) {
|
||||
public onEnableUserSignupModeChange(aEvent: MatSlideToggleChange) {
|
||||
this.putAdminSetting({
|
||||
key: PROPERTY_IS_USER_SIGNUP_ENABLED,
|
||||
value: aEvent.checked ? undefined : false
|
||||
});
|
||||
}
|
||||
|
||||
public onReadOnlyModeChange(aEvent: MatSlideToggleChange) {
|
||||
this.putAdminSetting({
|
||||
key: PROPERTY_IS_READ_ONLY_MODE,
|
||||
value: aEvent.checked ? true : undefined
|
||||
});
|
||||
}
|
||||
|
||||
public onSetSystemMessage() {
|
||||
const systemMessage = prompt($localize`Please set your system message:`);
|
||||
|
||||
|
@ -55,6 +55,18 @@
|
||||
</td>
|
||||
<td class="pl-1">{{ exchangeRate.label2 }}</td>
|
||||
<td>
|
||||
<a
|
||||
class="h-100 mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
[queryParams]="{
|
||||
assetProfileDialog: true,
|
||||
dataSource: exchangeRate.dataSource,
|
||||
symbol: exchangeRate.symbol
|
||||
}"
|
||||
[routerLink]="['/admin', 'market-data']"
|
||||
>
|
||||
<ion-icon name="create-outline"></ion-icon>
|
||||
</a>
|
||||
<button
|
||||
*ngIf="customCurrencies.includes(exchangeRate.label2)"
|
||||
class="h-100 mx-1 no-min-width px-2"
|
||||
@ -81,21 +93,23 @@
|
||||
<div class="d-flex my-3">
|
||||
<div class="w-50" i18n>User Signup</div>
|
||||
<div class="w-50">
|
||||
<mat-checkbox
|
||||
<mat-slide-toggle
|
||||
color="primary"
|
||||
hideIcon="true"
|
||||
[checked]="info.globalPermissions.includes(permissions.createUserAccount)"
|
||||
(change)="onEnableUserSignupModeChange($event)"
|
||||
></mat-checkbox>
|
||||
></mat-slide-toggle>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="hasPermissionToToggleReadOnlyMode" class="d-flex my-3">
|
||||
<div class="w-50" i18n>Read-only Mode</div>
|
||||
<div class="w-50">
|
||||
<mat-checkbox
|
||||
<mat-slide-toggle
|
||||
color="primary"
|
||||
hideIcon="true"
|
||||
[checked]="info?.isReadOnlyMode"
|
||||
(change)="onReadOnlyModeChange($event)"
|
||||
></mat-checkbox>
|
||||
></mat-slide-toggle>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="hasPermissionForSystemMessage" class="d-flex my-3">
|
||||
|
@ -3,8 +3,9 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { CacheService } from '@ghostfolio/client/services/cache.service';
|
||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||
|
||||
@ -18,10 +19,11 @@ import { AdminOverviewComponent } from './admin-overview.component';
|
||||
FormsModule,
|
||||
GfValueModule,
|
||||
MatButtonModule,
|
||||
MatCheckboxModule,
|
||||
MatCardModule,
|
||||
MatSelectModule,
|
||||
ReactiveFormsModule
|
||||
MatSlideToggleModule,
|
||||
ReactiveFormsModule,
|
||||
RouterModule
|
||||
],
|
||||
providers: [CacheService],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
|
@ -86,12 +86,16 @@
|
||||
</button>
|
||||
<mat-menu #platformMenu="matMenu" xPosition="before">
|
||||
<button mat-menu-item (click)="onUpdatePlatform(element)">
|
||||
<ion-icon class="mr-2" name="create-outline"></ion-icon>
|
||||
<span i18n>Edit</span>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="create-outline"></ion-icon>
|
||||
<span i18n>Edit</span>
|
||||
</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onDeletePlatform(element.id)">
|
||||
<ion-icon class="mr-2" name="trash-outline"></ion-icon>
|
||||
<span i18n>Delete</span>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="trash-outline"></ion-icon>
|
||||
<span i18n>Delete</span>
|
||||
</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
|
@ -66,12 +66,16 @@
|
||||
</button>
|
||||
<mat-menu #tagMenu="matMenu" xPosition="before">
|
||||
<button mat-menu-item (click)="onUpdateTag(element)">
|
||||
<ion-icon class="mr-2" name="create-outline"></ion-icon>
|
||||
<span i18n>Edit</span>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="create-outline"></ion-icon>
|
||||
<span i18n>Edit</span>
|
||||
</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onDeleteTag(element.id)">
|
||||
<ion-icon class="mr-2" name="trash-outline"></ion-icon>
|
||||
<span i18n>Delete</span>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="trash-outline"></ion-icon>
|
||||
<span i18n>Delete</span>
|
||||
</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
|
@ -203,16 +203,20 @@
|
||||
mat-menu-item
|
||||
(click)="onImpersonateUser(element.id)"
|
||||
>
|
||||
<ion-icon class="mr-2" name="contract-outline"></ion-icon>
|
||||
<span i18n>Impersonate User</span>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="contract-outline"></ion-icon>
|
||||
<span i18n>Impersonate User</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
[disabled]="element.id === user?.id"
|
||||
(click)="onDeleteUser(element.id)"
|
||||
>
|
||||
<ion-icon class="mr-2" name="trash-outline"></ion-icon>
|
||||
<span i18n>Delete User</span>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="trash-outline"></ion-icon>
|
||||
<span i18n>Delete User</span>
|
||||
</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
|
@ -1,6 +1,5 @@
|
||||
<button
|
||||
*ngIf="deviceType === 'mobile'"
|
||||
class="mt-2"
|
||||
mat-button
|
||||
(click)="onClickCloseButton()"
|
||||
>
|
||||
|
@ -1,7 +1,9 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
margin-bottom: 0;
|
||||
min-height: 0;
|
||||
padding: 0 !important;
|
||||
|
||||
@media (min-width: 576px) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { resolveFearAndGreedIndex } from '@ghostfolio/common/helper';
|
||||
import { translate } from '@ghostfolio/ui/i18n';
|
||||
|
||||
@Component({
|
||||
selector: 'gf-fear-and-greed-index',
|
||||
@ -24,9 +25,9 @@ export class FearAndGreedIndexComponent implements OnChanges, OnInit {
|
||||
public ngOnInit() {}
|
||||
|
||||
public ngOnChanges() {
|
||||
const { emoji, text } = resolveFearAndGreedIndex(this.fearAndGreedIndex);
|
||||
const { emoji, key } = resolveFearAndGreedIndex(this.fearAndGreedIndex);
|
||||
|
||||
this.fearAndGreedIndexEmoji = emoji;
|
||||
this.fearAndGreedIndexText = text;
|
||||
this.fearAndGreedIndexText = translate(key);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<div class="container justify-content-center p-3">
|
||||
<div *ngIf="user.settings.viewMode !== 'ZEN'" class="mb-3 text-center">
|
||||
<div class="mb-3 text-center">
|
||||
<gf-toggle
|
||||
[defaultValue]="user?.settings?.dateRange"
|
||||
[isLoading]="positions === undefined"
|
||||
|
@ -155,16 +155,18 @@
|
||||
[isDate]="true"
|
||||
[locale]="data.locale"
|
||||
[value]="firstBuyDate"
|
||||
>First Buy Date</gf-value
|
||||
>First Activity</gf-value
|
||||
>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<gf-value
|
||||
i18n
|
||||
size="medium"
|
||||
[locale]="data.locale"
|
||||
[value]="transactionCount"
|
||||
>Transactions</gf-value
|
||||
><ng-container *ngIf="transactionCount === 1">Activity</ng-container
|
||||
><ng-container *ngIf="transactionCount !== 1"
|
||||
>Activities</ng-container
|
||||
></gf-value
|
||||
>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
|
@ -1,6 +1,5 @@
|
||||
<div class="container">
|
||||
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Membership</h1>
|
||||
<div class="row">
|
||||
<div class="align-items-center container d-flex h-100 justify-content-center">
|
||||
<div class="row w-100">
|
||||
<div class="col">
|
||||
<div class="align-items-center d-flex flex-column">
|
||||
<gf-membership-card
|
||||
@ -34,7 +33,7 @@
|
||||
> <span i18n>per year</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="align-items-center d-flex justfiy-content-center mt-4">
|
||||
<div class="align-items-center d-flex justify-content-center mt-4">
|
||||
<a
|
||||
*ngIf="!user?.subscription?.expiresAt"
|
||||
class="mx-1"
|
||||
|
@ -1,6 +1,7 @@
|
||||
:host {
|
||||
color: rgb(var(--dark-primary-text));
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
|
@ -13,8 +13,8 @@ import { User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { Account as AccountModel } from '@prisma/client';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject, Subscription } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { EMPTY, Subject, Subscription } from 'rxjs';
|
||||
import { catchError, takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog/create-or-update-account-dialog.component';
|
||||
import { TransferBalanceDialog } from './transfer-balance/transfer-balance-dialog.component';
|
||||
@ -283,7 +283,6 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
||||
data: {
|
||||
accounts: this.accounts
|
||||
},
|
||||
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||
});
|
||||
|
||||
@ -301,7 +300,14 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
||||
accountIdTo,
|
||||
balance
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.pipe(
|
||||
catchError(() => {
|
||||
alert($localize`Oops, cash balance transfer has failed.`);
|
||||
|
||||
return EMPTY;
|
||||
}),
|
||||
takeUntil(this.unsubscribeSubject)
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.fetchAccounts();
|
||||
});
|
||||
|
@ -15,6 +15,7 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
|
||||
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { Currency } from '@ghostfolio/common/interfaces/currency.interface';
|
||||
import { Platform } from '@prisma/client';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { map, startWith } from 'rxjs/operators';
|
||||
@ -30,7 +31,7 @@ import { CreateOrUpdateAccountDialogParams } from './interfaces/interfaces';
|
||||
})
|
||||
export class CreateOrUpdateAccountDialog implements OnDestroy {
|
||||
public accountForm: FormGroup;
|
||||
public currencies: string[] = [];
|
||||
public currencies: Currency[] = [];
|
||||
public filteredPlatforms: Observable<Platform[]>;
|
||||
public platforms: Platform[];
|
||||
|
||||
@ -46,7 +47,10 @@ export class CreateOrUpdateAccountDialog implements OnDestroy {
|
||||
public ngOnInit() {
|
||||
const { currencies, platforms } = this.dataService.fetchInfo();
|
||||
|
||||
this.currencies = currencies;
|
||||
this.currencies = currencies.map((currency) => ({
|
||||
label: currency,
|
||||
value: currency
|
||||
}));
|
||||
this.platforms = platforms;
|
||||
|
||||
this.accountForm = this.formBuilder.group({
|
||||
@ -101,7 +105,7 @@ export class CreateOrUpdateAccountDialog implements OnDestroy {
|
||||
const account: CreateAccountDto | UpdateAccountDto = {
|
||||
balance: this.accountForm.controls['balance'].value,
|
||||
comment: this.accountForm.controls['comment'].value,
|
||||
currency: this.accountForm.controls['currency'].value,
|
||||
currency: this.accountForm.controls['currency'].value?.value,
|
||||
id: this.accountForm.controls['accountId'].value,
|
||||
isExcluded: this.accountForm.controls['isExcluded'].value,
|
||||
name: this.accountForm.controls['name'].value,
|
||||
|
@ -20,11 +20,10 @@
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Currency</mat-label>
|
||||
<mat-select formControlName="currency">
|
||||
<mat-option *ngFor="let currency of currencies" [value]="currency"
|
||||
>{{ currency }}</mat-option
|
||||
>
|
||||
</mat-select>
|
||||
<gf-currency-selector
|
||||
formControlName="currency"
|
||||
[currencies]="currencies"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
@ -37,7 +36,7 @@
|
||||
(keydown.enter)="$event.stopPropagation()"
|
||||
/>
|
||||
<span class="ml-2" matTextSuffix
|
||||
>{{ accountForm.controls['currency'].value }}</span
|
||||
>{{ accountForm.controls['currency']?.value?.value }}</span
|
||||
>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
@ -7,8 +7,8 @@ import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module';
|
||||
import { GfCurrencySelectorModule } from '@ghostfolio/ui/currency-selector/currency-selector.module';
|
||||
|
||||
import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.component';
|
||||
|
||||
@ -17,6 +17,7 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.c
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
GfCurrencySelectorModule,
|
||||
GfSymbolIconModule,
|
||||
MatAutocompleteModule,
|
||||
MatButtonModule,
|
||||
@ -24,7 +25,6 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.c
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
ReactiveFormsModule
|
||||
]
|
||||
})
|
||||
|
@ -4,7 +4,13 @@ import {
|
||||
Inject,
|
||||
OnDestroy
|
||||
} from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import {
|
||||
AbstractControl,
|
||||
FormBuilder,
|
||||
FormGroup,
|
||||
ValidationErrors,
|
||||
Validators
|
||||
} from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { TransferBalanceDto } from '@ghostfolio/api/app/account/transfer-balance.dto';
|
||||
import { Account } from '@prisma/client';
|
||||
@ -35,11 +41,16 @@ export class TransferBalanceDialog implements OnDestroy {
|
||||
public ngOnInit() {
|
||||
this.accounts = this.data.accounts;
|
||||
|
||||
this.transferBalanceForm = this.formBuilder.group({
|
||||
balance: [0, Validators.required],
|
||||
fromAccount: ['', Validators.required],
|
||||
toAccount: ['', Validators.required]
|
||||
});
|
||||
this.transferBalanceForm = this.formBuilder.group(
|
||||
{
|
||||
balance: ['', Validators.required],
|
||||
fromAccount: ['', Validators.required],
|
||||
toAccount: ['', Validators.required]
|
||||
},
|
||||
{
|
||||
validators: this.compareAccounts
|
||||
}
|
||||
);
|
||||
|
||||
this.transferBalanceForm.get('fromAccount').valueChanges.subscribe((id) => {
|
||||
this.currency = this.accounts.find((account) => {
|
||||
@ -66,4 +77,13 @@ export class TransferBalanceDialog implements OnDestroy {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
private compareAccounts(control: AbstractControl): ValidationErrors {
|
||||
const accountFrom = control.get('fromAccount');
|
||||
const accountTo = control.get('toAccount');
|
||||
|
||||
if (accountFrom.value === accountTo.value) {
|
||||
return { invalid: true };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,15 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
imports: [MatButtonModule, RouterModule],
|
||||
selector: 'gf-hacktoberfest-2023-debriefing-page',
|
||||
standalone: true,
|
||||
templateUrl: './hacktoberfest-2023-debriefing-page.html'
|
||||
})
|
||||
export class Hacktoberfest2023DebriefingPageComponent {
|
||||
public routerLinkAbout = ['/' + $localize`about`];
|
||||
public routerLinkFeatures = ['/' + $localize`features`];
|
||||
}
|
@ -0,0 +1,283 @@
|
||||
<div class="blog container">
|
||||
<div class="row">
|
||||
<div class="col-md-8 offset-md-2">
|
||||
<article>
|
||||
<div class="mb-4 text-center">
|
||||
<h1 class="mb-1">Hacktoberfest 2023 Debriefing</h1>
|
||||
<div class="mb-3 text-muted"><small>2023-11-05</small></div>
|
||||
<img
|
||||
alt="Hacktoberfest 2023 with Ghostfolio Teaser"
|
||||
class="rounded w-100"
|
||||
src="../assets/images/blog/hacktoberfest-2023.png"
|
||||
title="Hacktoberfest 2023 with Ghostfolio"
|
||||
/>
|
||||
</div>
|
||||
<section class="mb-4">
|
||||
<p>
|
||||
As <a href="https://hacktoberfest.com">Hacktoberfest</a> has come to
|
||||
an end, it’s time to look back and share our learnings.
|
||||
Hacktoberfest is a month-long celebration of open source software
|
||||
(OSS) projects, their maintainers, and the entire community of
|
||||
contributors. Each October, open source maintainers from all over
|
||||
the world give extra attention to new contributors while guiding
|
||||
them through their first pull requests on
|
||||
<a href="https://github.com/ghostfolio/ghostfolio">GitHub</a>. This
|
||||
year the event celebrated its 10th anniversary. At Ghostfolio, we
|
||||
have participated in the event for the
|
||||
<a href="../en/blog/2023/09/hacktoberfest-2023">second time</a> this
|
||||
year.
|
||||
</p>
|
||||
<p>
|
||||
In this debriefing, we’ll take a closer look at our journey during
|
||||
Hacktoberfest, exploring the facts and figures, key takeaways, and
|
||||
the impact on Ghostfolio.
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<h2 class="h4">Hacktoberfest with Ghostfolio</h2>
|
||||
<p>
|
||||
<a href="https://ghostfol.io">Ghostfolio</a> is a modern web
|
||||
application for managing personal finances. The software aggregates
|
||||
your assets and empowers informed decision-making to help you
|
||||
balance your portfolio or plan for future investments.
|
||||
</p>
|
||||
<p>
|
||||
Our experience at Ghostfolio during Hacktoberfest 2023 has been
|
||||
truly remarkable. Despite the absence of T-shirt rewards this year,
|
||||
our expectations were exceeded in every way.
|
||||
<a [routerLink]="routerLinkAbout">We</a> had the privilege of
|
||||
collaborating with
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio/graphs/contributors"
|
||||
>20 talented developers</a
|
||||
>
|
||||
from around the world, each bringing their unique skills and
|
||||
backgrounds to our fintech project.
|
||||
</p>
|
||||
<p>
|
||||
<figure class="figure">
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio/graphs/contributors"
|
||||
>
|
||||
<img
|
||||
alt="Screenshot of the Ghostfolio’s Hacktoberfest 2023 Insights"
|
||||
class="figure-img img-fluid rounded"
|
||||
src="../assets/images/blog/hacktoberfest-2023-insights.png"
|
||||
title="Screenshot of the Ghostfolio’s Hacktoberfest 2023 Insights"
|
||||
/>
|
||||
</a>
|
||||
<figcaption class="figure-caption text-center">
|
||||
Screenshot of the Ghostfolio’s Hacktoberfest 2023 Insights
|
||||
</figcaption>
|
||||
</figure>
|
||||
</p>
|
||||
<p>
|
||||
All these contributions made during Hacktoberfest have a significant
|
||||
impact on Ghostfolio. As many as 100 new
|
||||
<a [routerLink]="routerLinkFeatures">features</a> and improvements
|
||||
have been merged to enhance the user experience and software
|
||||
platform management.
|
||||
</p>
|
||||
<p class="text-center">
|
||||
<img
|
||||
alt="Hacktoberfest 2023 Badges"
|
||||
class="figure-img img-fluid rounded w-50"
|
||||
src="../assets/images/blog/hacktoberfest-2023-badges.png"
|
||||
title="Hacktoberfest 2023 Badges"
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
The lessons learned through this global collaboration highlight the
|
||||
collective spirit of the open source community, even without
|
||||
traditional incentives. It serves as a driving force for Ghostfolio
|
||||
to evolve into an exceptional piece of open source wealth management
|
||||
software. Hacktoberfest 2023 has been an amazing and enlightening
|
||||
journey for everyone involved.
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<h2 class="h4">Key Takeaways</h2>
|
||||
<p>
|
||||
We’ve gathered some valuable takeaways from our experience to
|
||||
consider for upcoming years. These insights can help us and fellow
|
||||
open source projects make the most of Hacktoberfest:
|
||||
</p>
|
||||
<ul class="list-unstyled">
|
||||
<li>
|
||||
<h3 class="h5">Prepare early</h3>
|
||||
<p>
|
||||
Prospective contributors often begin looking for tasks as early
|
||||
as the end of September. Preparing your project, organizing
|
||||
issues, and setting clear goals in advance can help you attract
|
||||
and engage more participants.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<h3 class="h5">Meet formal requirements</h3>
|
||||
<p>
|
||||
Ensure that your project aligns with Hacktoberfest’s formal
|
||||
requirements and rules. Properly tag issues, provide clear
|
||||
guidelines for contributions, and make sure your project is
|
||||
accessible to newcomers.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<h3 class="h5">Allocate sufficient time</h3>
|
||||
<p>
|
||||
Being responsive and providing support to participants is
|
||||
crucial. Allocate enough time to answer questions, review and
|
||||
merge pull requests, and offer guidance to first-time
|
||||
contributors. This level of support can significantly enhance
|
||||
the experience for both contributors and maintainers.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<h3 class="h5">Provide clarity in descriptions</h3>
|
||||
<p>
|
||||
When creating issues for Hacktoberfest, be as clear as possible
|
||||
in your descriptions. Including screenshots, links to code
|
||||
examples, and detailed instructions can make it easier for
|
||||
contributors to understand and complete the task successfully.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<h3 class="h5">Isolate tasks</h3>
|
||||
<p>
|
||||
Ideally, focus on tasks that are approachable for beginners, as
|
||||
the setup and frameworks used can already be quite challenging.
|
||||
This way, you can attract a wider range of contributors.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<h3 class="h5">Embrace learning</h3>
|
||||
<p>
|
||||
Understand that not every attempt will result in a successful
|
||||
contribution. It’s okay if a contributor’s pull request doesn’t
|
||||
make it into the project. Encourage a learning mindset and
|
||||
provide constructive feedback, helping contributors grow and
|
||||
improve over time.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
By following these takeaways, we can ensure a rewarding experience
|
||||
for everyone taking part in future Hacktoberfest events.
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<h2 class="h4">Thank you</h2>
|
||||
<p>
|
||||
Our experience with Hacktoberfest truly showcases the magic of open
|
||||
source. The sense of community, mentorship, and our shared
|
||||
dedication to software development is what motivates us at
|
||||
Ghostfolio. As we look forward to future collaborations to make
|
||||
personal finance and investing more accessible, we simply want to
|
||||
thank everyone involved.
|
||||
</p>
|
||||
<p>
|
||||
Keep coding and sharing your learnings!<br />
|
||||
Thomas from Ghostfolio
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<ul class="list-inline">
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Code</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Collaboration</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Community</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Development</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Feature</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Finance</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Fintech</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Ghostfolio</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">GitHub</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Hacktoberfest</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Investing</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Investment</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Learning</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Lessons learned</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Mindset</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">October</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Open Source</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">OSS</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Personal Finance</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Portfolio</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Programming</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Software</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Takeaways</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">User Experience</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">UX</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Wealth</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Wealth Management</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<a i18n [routerLink]="['/blog']">Blog</a>
|
||||
</li>
|
||||
<li
|
||||
aria-current="page"
|
||||
class="active breadcrumb-item text-truncate"
|
||||
>
|
||||
Hacktoberfest 2023 Debriefing
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -163,6 +163,15 @@ const routes: Routes = [
|
||||
'./2023/09/hacktoberfest-2023/hacktoberfest-2023-page.component'
|
||||
).then((c) => c.Hacktoberfest2023PageComponent),
|
||||
title: 'Hacktoberfest 2023'
|
||||
},
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
path: '2023/11/hacktoberfest-2023-debriefing',
|
||||
loadComponent: () =>
|
||||
import(
|
||||
'./2023/11/hacktoberfest-2023/hacktoberfest-2023-debriefing-page.component'
|
||||
).then((c) => c.Hacktoberfest2023DebriefingPageComponent),
|
||||
title: 'Hacktoberfest 2023 Debriefing'
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -8,6 +8,32 @@
|
||||
finance</small
|
||||
>
|
||||
</h1>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-content>
|
||||
<div class="container p-0">
|
||||
<div class="flex-nowrap no-gutters row">
|
||||
<a
|
||||
class="d-flex overflow-hidden w-100"
|
||||
href="../en/blog/2023/11/hacktoberfest-2023-debriefing"
|
||||
>
|
||||
<div class="flex-grow-1 overflow-hidden">
|
||||
<div class="h6 m-0 text-truncate">
|
||||
Hacktoberfest 2023 Debriefing
|
||||
</div>
|
||||
<div class="d-flex text-muted">2023-11-05</div>
|
||||
</div>
|
||||
<div class="align-items-center d-flex">
|
||||
<ion-icon
|
||||
class="chevron text-muted"
|
||||
name="chevron-forward-outline"
|
||||
size="small"
|
||||
></ion-icon>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-content>
|
||||
<div class="container p-0">
|
||||
|
@ -5,6 +5,11 @@
|
||||
Ghostfolio is a personal finance dashboard to keep track of your assets
|
||||
like stocks, ETFs or cryptocurrencies across multiple platforms.
|
||||
</li>
|
||||
<li i18n="@@metaKeywords">
|
||||
app, asset, cryptocurrency, dashboard, etf, finance, management,
|
||||
performance, portfolio, software, stock, trading, wealth, web3
|
||||
</li>
|
||||
<li i18n="@@slogan">Open Source Wealth Management Software</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -309,7 +309,6 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
private update() {
|
||||
this.isLoadingBenchmarkComparator = true;
|
||||
this.isLoadingInvestmentChart = true;
|
||||
|
||||
this.dataService
|
||||
@ -385,35 +384,37 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
private updateBenchmarkDataItems() {
|
||||
this.benchmarkDataItems = [];
|
||||
|
||||
if (this.user.settings.benchmark) {
|
||||
const { dataSource, symbol } =
|
||||
this.benchmarks.find(({ id }) => {
|
||||
return id === this.user.settings.benchmark;
|
||||
}) ?? {};
|
||||
|
||||
this.dataService
|
||||
.fetchBenchmarkBySymbol({
|
||||
dataSource,
|
||||
symbol,
|
||||
startDate: this.firstOrderDate
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ marketData }) => {
|
||||
this.benchmarkDataItems = marketData.map(({ date, value }) => {
|
||||
return {
|
||||
date,
|
||||
value
|
||||
};
|
||||
if (dataSource && symbol) {
|
||||
this.isLoadingBenchmarkComparator = true;
|
||||
|
||||
this.dataService
|
||||
.fetchBenchmarkBySymbol({
|
||||
dataSource,
|
||||
symbol,
|
||||
startDate: this.firstOrderDate
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ marketData }) => {
|
||||
this.benchmarkDataItems = marketData.map(({ date, value }) => {
|
||||
return {
|
||||
date,
|
||||
value
|
||||
};
|
||||
});
|
||||
|
||||
this.isLoadingBenchmarkComparator = false;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.isLoadingBenchmarkComparator = false;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
} else {
|
||||
this.benchmarkDataItems = [];
|
||||
|
||||
this.isLoadingBenchmarkComparator = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
<div class="container">
|
||||
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Analysis</h1>
|
||||
<div *ngIf="user?.settings?.viewMode !== 'ZEN'" class="my-4 text-center">
|
||||
<div class="my-4 text-center">
|
||||
<gf-toggle
|
||||
[defaultValue]="user?.settings?.dateRange"
|
||||
[isLoading]="isLoadingBenchmarkComparator"
|
||||
[isLoading]="isLoadingBenchmarkComparator || isLoadingInvestmentChart"
|
||||
[options]="dateRangeOptions"
|
||||
(change)="onChangeDateRange($event.value)"
|
||||
></gf-toggle>
|
||||
@ -23,7 +23,7 @@
|
||||
[benchmarks]="benchmarks"
|
||||
[colorScheme]="user?.settings?.colorScheme"
|
||||
[daysInMarket]="daysInMarket"
|
||||
[isLoading]="isLoadingBenchmarkComparator"
|
||||
[isLoading]="isLoadingBenchmarkComparator || isLoadingInvestmentChart"
|
||||
[locale]="user?.settings?.locale"
|
||||
[performanceDataItems]="performanceDataItemsInPercentage"
|
||||
[user]="user"
|
||||
@ -149,7 +149,7 @@
|
||||
[daysInMarket]="daysInMarket"
|
||||
[historicalDataItems]="performanceDataItems"
|
||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||
[isLoading]="isLoadingBenchmarkComparator"
|
||||
[isLoading]="isLoadingInvestmentChart"
|
||||
[locale]="user?.settings?.locale"
|
||||
[range]="user?.settings?.dateRange"
|
||||
></gf-investment-chart>
|
||||
|
@ -16,10 +16,10 @@ const routes: Routes = [
|
||||
.filter(({ key }) => {
|
||||
return key !== 'ghostfolio';
|
||||
})
|
||||
.map(({ component, key, name }) => {
|
||||
.map(({ alias, component, key, name }) => {
|
||||
return {
|
||||
canActivate: [AuthGuard],
|
||||
path: $localize`open-source-alternative-to` + `-${key}`,
|
||||
path: $localize`open-source-alternative-to` + `-${alias ?? key}`,
|
||||
loadComponent: () =>
|
||||
import(`./products/${key}-page.component`).then(() => component),
|
||||
title: $localize`Open Source Alternative to ${name}`
|
||||
|
@ -12,9 +12,13 @@ import { products } from './products';
|
||||
export class PersonalFinanceToolsPageComponent implements OnDestroy {
|
||||
public pathAlternativeTo = $localize`open-source-alternative-to` + '-';
|
||||
public pathResources = '/' + $localize`resources`;
|
||||
public products = products.filter(({ key }) => {
|
||||
return key !== 'ghostfolio';
|
||||
});
|
||||
public products = products
|
||||
.filter(({ key }) => {
|
||||
return key !== 'ghostfolio';
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
|
||||
});
|
||||
public routerLinkAbout = ['/' + $localize`about`];
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
@ -28,8 +28,8 @@
|
||||
<div class="flex-nowrap no-gutters row">
|
||||
<a
|
||||
class="d-flex overflow-hidden w-100"
|
||||
title="Compare Ghostfolio to {{ product.name }}"
|
||||
[routerLink]="[pathResources, 'personal-finance-tools', pathAlternativeTo + product.key]"
|
||||
title="Compare Ghostfolio to {{ product.name }} - {{ product.slogan }}"
|
||||
[routerLink]="[pathResources, 'personal-finance-tools', pathAlternativeTo + (product.alias ?? product.key)]"
|
||||
>
|
||||
<div class="flex-grow-1 overflow-hidden">
|
||||
<div class="h6 m-0 text-truncate" i18n>
|
||||
|
@ -35,15 +35,19 @@
|
||||
its capabilities, security, and user experience.
|
||||
</p>
|
||||
<p i18n>
|
||||
Let’s dive deeper into the detailed comparison table below to gain a
|
||||
thorough understanding of how Ghostfolio positions itself relative
|
||||
to {{ product2.name }}. We will explore various aspects such as
|
||||
features, data privacy, pricing, and more, allowing you to make a
|
||||
well-informed choice for your personal requirements.
|
||||
Let’s dive deeper into the detailed Ghostfolio vs {{ product2.name
|
||||
}} comparison table below to gain a thorough understanding of how
|
||||
Ghostfolio positions itself relative to {{ product2.name }}. We will
|
||||
explore various aspects such as features, data privacy, pricing, and
|
||||
more, allowing you to make a well-informed choice for your personal
|
||||
requirements.
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<table class="gf-table w-100">
|
||||
<caption class="text-center" i18n>
|
||||
Ghostfolio vs {{ product2.name }} comparison table
|
||||
</caption>
|
||||
<thead>
|
||||
<tr class="mat-mdc-header-row">
|
||||
<th class="mat-mdc-header-cell px-1 py-2"></th>
|
||||
@ -197,12 +201,13 @@
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<p i18n>
|
||||
Please note that the information provided is based on our
|
||||
independent research and analysis. This website is not affiliated
|
||||
with {{ product2.name }} or any other product mentioned in the
|
||||
comparison. As the landscape of personal finance tools evolves, it
|
||||
is essential to verify any specific details or changes directly from
|
||||
the respective product page. Data needs a refresh? Help us maintain
|
||||
Please note that the information provided in the Ghostfolio vs {{
|
||||
product2.name }} comparison table is based on our independent
|
||||
research and analysis. This website is not affiliated with {{
|
||||
product2.name }} or any other product mentioned in the comparison.
|
||||
As the landscape of personal finance tools evolves, it is essential
|
||||
to verify any specific details or changes directly from the
|
||||
respective product page. Data needs a refresh? Help us maintain
|
||||
accurate data on
|
||||
<a href="https://github.com/ghostfolio/ghostfolio">GitHub</a>.
|
||||
</p>
|
||||
@ -291,7 +296,7 @@
|
||||
aria-current="page"
|
||||
class="active breadcrumb-item text-truncate"
|
||||
>
|
||||
{{ product2.name }}
|
||||
Ghostfolio vs {{ product2.name }}
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
@ -1,15 +1,20 @@
|
||||
import { Product } from '@ghostfolio/common/interfaces';
|
||||
|
||||
import { AltooPageComponent } from './products/altoo-page.component';
|
||||
import { BeanvestPageComponent } from './products/beanvest-page.component';
|
||||
import { CapitallyPageComponent } from './products/capitally-page.component';
|
||||
import { CapMonPageComponent } from './products/capmon-page.component';
|
||||
import { CopilotMoneyPageComponent } from './products/copilot-money-page.component';
|
||||
import { DeltaPageComponent } from './products/delta-page.component';
|
||||
import { DivvyDiaryPageComponent } from './products/divvydiary-page.component';
|
||||
import { EightFiguresPageComponent } from './products/eightfigures-page.component';
|
||||
import { ExirioPageComponent } from './products/exirio-page.component';
|
||||
import { FinaryPageComponent } from './products/finary-page.component';
|
||||
import { FinWisePageComponent } from './products/finwise-page.component';
|
||||
import { FolisharePageComponent } from './products/folishare-page.component';
|
||||
import { GetquinPageComponent } from './products/getquin-page.component';
|
||||
import { GoSpatzPageComponent } from './products/gospatz-page.component';
|
||||
import { IntuitMintPageComponent } from './products/intuit-mint-page.component';
|
||||
import { JustEtfPageComponent } from './products/justetf-page.component';
|
||||
import { KuberaPageComponent } from './products/kubera-page.component';
|
||||
import { MarketsShPageComponent } from './products/markets.sh-page.component';
|
||||
@ -20,6 +25,7 @@ import { PlannixPageComponent } from './products/plannix-page.component';
|
||||
import { PortfolioDividendTrackerPageComponent } from './products/portfolio-dividend-tracker-page.component';
|
||||
import { PortseidoPageComponent } from './products/portseido-page.component';
|
||||
import { ProjectionLabPageComponent } from './products/projectionlab-page.component';
|
||||
import { RocketMoneyPageComponent } from './products/rocket-money-page.component';
|
||||
import { SeekingAlphaPageComponent } from './products/seeking-alpha-page.component';
|
||||
import { SharesightPageComponent } from './products/sharesight-page.component';
|
||||
import { SimplePortfolioPageComponent } from './products/simple-portfolio-page.component';
|
||||
@ -28,6 +34,8 @@ import { StocklePageComponent } from './products/stockle-page.component';
|
||||
import { StockMarketEyePageComponent } from './products/stockmarketeye-page.component';
|
||||
import { SumioPageComponent } from './products/sumio-page.component';
|
||||
import { UtlunaPageComponent } from './products/utluna-page.component';
|
||||
import { VyzerPageComponent } from './products/vyzer-page.component';
|
||||
import { WealthicaPageComponent } from './products/wealthica-page.component';
|
||||
import { YeekateePageComponent } from './products/yeekatee-page.component';
|
||||
|
||||
export const products: Product[] = [
|
||||
@ -63,13 +71,34 @@ export const products: Product[] = [
|
||||
origin: $localize`Switzerland`,
|
||||
slogan: 'Simplicity for Complex Wealth'
|
||||
},
|
||||
{
|
||||
component: BeanvestPageComponent,
|
||||
founded: 2020,
|
||||
hasFreePlan: true,
|
||||
hasSelfHostingAbility: false,
|
||||
key: 'beanvest',
|
||||
name: 'Beanvest',
|
||||
origin: $localize`France`,
|
||||
pricingPerYear: '$100',
|
||||
slogan: 'Stock Portfolio Tracker for Smart Investors'
|
||||
},
|
||||
{
|
||||
component: CapitallyPageComponent,
|
||||
hasFreePlan: true,
|
||||
hasSelfHostingAbility: false,
|
||||
key: 'capitally',
|
||||
name: 'Capitally',
|
||||
origin: $localize`Poland`,
|
||||
pricingPerYear: '€50',
|
||||
slogan: 'Optimize your investments performance'
|
||||
},
|
||||
{
|
||||
component: CapMonPageComponent,
|
||||
founded: 2022,
|
||||
key: 'capmon',
|
||||
name: 'CapMon.org',
|
||||
origin: $localize`Germany`,
|
||||
note: 'Sunset in 2023',
|
||||
note: 'CapMon.org has discontinued in 2023',
|
||||
slogan: 'Next Generation Assets Tracking'
|
||||
},
|
||||
{
|
||||
@ -106,6 +135,15 @@ export const products: Product[] = [
|
||||
pricingPerYear: '€65',
|
||||
slogan: 'Your personal Dividend Calendar'
|
||||
},
|
||||
{
|
||||
alias: '8figures',
|
||||
component: EightFiguresPageComponent,
|
||||
founded: 2022,
|
||||
key: 'eightfigures',
|
||||
name: '8FIGURES',
|
||||
origin: $localize`United States`,
|
||||
slogan: 'Portfolio Tracker Designed by Professional Investors'
|
||||
},
|
||||
{
|
||||
component: ExirioPageComponent,
|
||||
founded: 2020,
|
||||
@ -126,6 +164,16 @@ export const products: Product[] = [
|
||||
origin: $localize`United States`,
|
||||
slogan: 'Real-Time Portfolio Tracker & Stock Tracker'
|
||||
},
|
||||
{
|
||||
component: FinWisePageComponent,
|
||||
founded: 2023,
|
||||
hasFreePlan: true,
|
||||
key: 'finwise',
|
||||
name: 'FinWise',
|
||||
origin: $localize`South Africa`,
|
||||
pricingPerYear: '€69.99',
|
||||
slogan: 'Personal finances, simplified'
|
||||
},
|
||||
{
|
||||
component: FolisharePageComponent,
|
||||
hasFreePlan: true,
|
||||
@ -158,6 +206,17 @@ export const products: Product[] = [
|
||||
origin: $localize`Germany`,
|
||||
slogan: 'Volle Kontrolle über deine Investitionen'
|
||||
},
|
||||
{
|
||||
component: IntuitMintPageComponent,
|
||||
hasFreePlan: true,
|
||||
hasSelfHostingAbility: false,
|
||||
key: 'intuit-mint',
|
||||
name: 'Intuit Mint',
|
||||
note: 'Intuit Mint has discontinued in 2023',
|
||||
origin: $localize`United States`,
|
||||
pricingPerYear: '$60',
|
||||
slogan: 'Managing money, made simple'
|
||||
},
|
||||
{
|
||||
component: JustEtfPageComponent,
|
||||
founded: 2011,
|
||||
@ -200,7 +259,7 @@ export const products: Product[] = [
|
||||
key: 'maybe-finance',
|
||||
languages: ['English'],
|
||||
name: 'Maybe Finance',
|
||||
note: 'Sunset in 2023',
|
||||
note: 'Maybe Finance has discontinued in 2023',
|
||||
origin: $localize`United States`,
|
||||
pricingPerYear: '$145',
|
||||
region: $localize`United States`,
|
||||
@ -271,6 +330,15 @@ export const products: Product[] = [
|
||||
pricingPerYear: '$108',
|
||||
slogan: 'Build Financial Plans You Love.'
|
||||
},
|
||||
{
|
||||
component: RocketMoneyPageComponent,
|
||||
founded: 2015,
|
||||
hasSelfHostingAbility: false,
|
||||
key: 'rocket-money',
|
||||
name: 'Rocket Money',
|
||||
origin: $localize`United States`,
|
||||
slogan: 'Track your net worth'
|
||||
},
|
||||
{
|
||||
component: SeekingAlphaPageComponent,
|
||||
founded: 2004,
|
||||
@ -328,7 +396,7 @@ export const products: Product[] = [
|
||||
key: 'stockmarketeye',
|
||||
name: 'StockMarketEye',
|
||||
origin: $localize`France`,
|
||||
note: 'Sunset in 2023',
|
||||
note: 'StockMarketEye has discontinued in 2023',
|
||||
slogan: 'A Powerful Portfolio & Investment Tracking App'
|
||||
},
|
||||
{
|
||||
@ -353,6 +421,28 @@ export const products: Product[] = [
|
||||
slogan: 'Your Portfolio. Revealed.',
|
||||
useAnonymously: true
|
||||
},
|
||||
{
|
||||
component: VyzerPageComponent,
|
||||
founded: 2020,
|
||||
hasFreePlan: true,
|
||||
key: 'vyzer',
|
||||
name: 'Vyzer',
|
||||
origin: $localize`United States`,
|
||||
pricingPerYear: '$348',
|
||||
slogan: 'Virtual Family Office for Smart Wealth Management'
|
||||
},
|
||||
{
|
||||
component: WealthicaPageComponent,
|
||||
founded: 2015,
|
||||
hasFreePlan: true,
|
||||
hasSelfHostingAbility: false,
|
||||
key: 'wealthica',
|
||||
languages: ['English', 'Français'],
|
||||
name: 'Wealthica',
|
||||
origin: $localize`Canada`,
|
||||
pricingPerYear: '$50',
|
||||
slogan: 'See all your investments in one place'
|
||||
},
|
||||
{
|
||||
component: YeekateePageComponent,
|
||||
founded: 2021,
|
||||
|
@ -0,0 +1,31 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { products } from '../products';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
imports: [CommonModule, MatButtonModule, RouterModule],
|
||||
selector: 'gf-beanvest-page',
|
||||
standalone: true,
|
||||
styleUrls: ['../product-page-template.scss'],
|
||||
templateUrl: '../product-page-template.html'
|
||||
})
|
||||
export class BeanvestPageComponent {
|
||||
public product1 = products.find(({ key }) => {
|
||||
return key === 'ghostfolio';
|
||||
});
|
||||
|
||||
public product2 = products.find(({ key }) => {
|
||||
return key === 'beanvest';
|
||||
});
|
||||
|
||||
public routerLinkAbout = ['/' + $localize`about`];
|
||||
public routerLinkFeatures = ['/' + $localize`features`];
|
||||
public routerLinkResourcesPersonalFinanceTools = [
|
||||
'/' + $localize`resources`,
|
||||
'personal-finance-tools'
|
||||
];
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { products } from '../products';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
imports: [CommonModule, MatButtonModule, RouterModule],
|
||||
selector: 'gf-capitally-page',
|
||||
standalone: true,
|
||||
styleUrls: ['../product-page-template.scss'],
|
||||
templateUrl: '../product-page-template.html'
|
||||
})
|
||||
export class CapitallyPageComponent {
|
||||
public product1 = products.find(({ key }) => {
|
||||
return key === 'ghostfolio';
|
||||
});
|
||||
|
||||
public product2 = products.find(({ key }) => {
|
||||
return key === 'capitally';
|
||||
});
|
||||
|
||||
public routerLinkAbout = ['/' + $localize`about`];
|
||||
public routerLinkFeatures = ['/' + $localize`features`];
|
||||
public routerLinkResourcesPersonalFinanceTools = [
|
||||
'/' + $localize`resources`,
|
||||
'personal-finance-tools'
|
||||
];
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { products } from '../products';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
imports: [CommonModule, MatButtonModule, RouterModule],
|
||||
selector: 'gf-eightfigures-page',
|
||||
standalone: true,
|
||||
styleUrls: ['../product-page-template.scss'],
|
||||
templateUrl: '../product-page-template.html'
|
||||
})
|
||||
export class EightFiguresPageComponent {
|
||||
public product1 = products.find(({ key }) => {
|
||||
return key === 'ghostfolio';
|
||||
});
|
||||
|
||||
public product2 = products.find(({ key }) => {
|
||||
return key === 'eightfigures';
|
||||
});
|
||||
|
||||
public routerLinkAbout = ['/' + $localize`about`];
|
||||
public routerLinkFeatures = ['/' + $localize`features`];
|
||||
public routerLinkResourcesPersonalFinanceTools = [
|
||||
'/' + $localize`resources`,
|
||||
'personal-finance-tools'
|
||||
];
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { products } from '../products';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
imports: [CommonModule, MatButtonModule, RouterModule],
|
||||
selector: 'gf-finwise-page',
|
||||
standalone: true,
|
||||
styleUrls: ['../product-page-template.scss'],
|
||||
templateUrl: '../product-page-template.html'
|
||||
})
|
||||
export class FinWisePageComponent {
|
||||
public product1 = products.find(({ key }) => {
|
||||
return key === 'ghostfolio';
|
||||
});
|
||||
|
||||
public product2 = products.find(({ key }) => {
|
||||
return key === 'finwise';
|
||||
});
|
||||
|
||||
public routerLinkAbout = ['/' + $localize`about`];
|
||||
public routerLinkFeatures = ['/' + $localize`features`];
|
||||
public routerLinkResourcesPersonalFinanceTools = [
|
||||
'/' + $localize`resources`,
|
||||
'personal-finance-tools'
|
||||
];
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { products } from '../products';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
imports: [CommonModule, MatButtonModule, RouterModule],
|
||||
selector: 'gf-intuit-mint-page',
|
||||
standalone: true,
|
||||
styleUrls: ['../product-page-template.scss'],
|
||||
templateUrl: '../product-page-template.html'
|
||||
})
|
||||
export class IntuitMintPageComponent {
|
||||
public product1 = products.find(({ key }) => {
|
||||
return key === 'ghostfolio';
|
||||
});
|
||||
|
||||
public product2 = products.find(({ key }) => {
|
||||
return key === 'intuit-mint';
|
||||
});
|
||||
|
||||
public routerLinkAbout = ['/' + $localize`about`];
|
||||
public routerLinkFeatures = ['/' + $localize`features`];
|
||||
public routerLinkResourcesPersonalFinanceTools = [
|
||||
'/' + $localize`resources`,
|
||||
'personal-finance-tools'
|
||||
];
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { products } from '../products';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
imports: [CommonModule, MatButtonModule, RouterModule],
|
||||
selector: 'gf-rocket-money-page',
|
||||
standalone: true,
|
||||
styleUrls: ['../product-page-template.scss'],
|
||||
templateUrl: '../product-page-template.html'
|
||||
})
|
||||
export class RocketMoneyPageComponent {
|
||||
public product1 = products.find(({ key }) => {
|
||||
return key === 'ghostfolio';
|
||||
});
|
||||
|
||||
public product2 = products.find(({ key }) => {
|
||||
return key === 'rocket-money';
|
||||
});
|
||||
|
||||
public routerLinkAbout = ['/' + $localize`about`];
|
||||
public routerLinkFeatures = ['/' + $localize`features`];
|
||||
public routerLinkResourcesPersonalFinanceTools = [
|
||||
'/' + $localize`resources`,
|
||||
'personal-finance-tools'
|
||||
];
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { products } from '../products';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
imports: [CommonModule, MatButtonModule, RouterModule],
|
||||
selector: 'gf-vyzer-page',
|
||||
standalone: true,
|
||||
styleUrls: ['../product-page-template.scss'],
|
||||
templateUrl: '../product-page-template.html'
|
||||
})
|
||||
export class VyzerPageComponent {
|
||||
public product1 = products.find(({ key }) => {
|
||||
return key === 'ghostfolio';
|
||||
});
|
||||
|
||||
public product2 = products.find(({ key }) => {
|
||||
return key === 'vyzer';
|
||||
});
|
||||
|
||||
public routerLinkAbout = ['/' + $localize`about`];
|
||||
public routerLinkFeatures = ['/' + $localize`features`];
|
||||
public routerLinkResourcesPersonalFinanceTools = [
|
||||
'/' + $localize`resources`,
|
||||
'personal-finance-tools'
|
||||
];
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { products } from '../products';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
imports: [CommonModule, MatButtonModule, RouterModule],
|
||||
selector: 'gf-wealthica-page',
|
||||
standalone: true,
|
||||
styleUrls: ['../product-page-template.scss'],
|
||||
templateUrl: '../product-page-template.html'
|
||||
})
|
||||
export class WealthicaPageComponent {
|
||||
public product1 = products.find(({ key }) => {
|
||||
return key === 'ghostfolio';
|
||||
});
|
||||
|
||||
public product2 = products.find(({ key }) => {
|
||||
return key === 'wealthica';
|
||||
});
|
||||
|
||||
public routerLinkAbout = ['/' + $localize`about`];
|
||||
public routerLinkFeatures = ['/' + $localize`features`];
|
||||
public routerLinkResourcesPersonalFinanceTools = [
|
||||
'/' + $localize`resources`,
|
||||
'personal-finance-tools'
|
||||
];
|
||||
}
|
@ -2,7 +2,6 @@ import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto';
|
||||
import { UpdateBulkMarketDataDto } from '@ghostfolio/api/app/admin/update-bulk-market-data.dto';
|
||||
import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-data.dto';
|
||||
import { CreatePlatformDto } from '@ghostfolio/api/app/platform/create-platform.dto';
|
||||
import { UpdatePlatformDto } from '@ghostfolio/api/app/platform/update-platform.dto';
|
||||
import { CreateTagDto } from '@ghostfolio/api/app/tag/create-tag.dto';
|
||||
@ -203,15 +202,25 @@ export class AdminService {
|
||||
}
|
||||
|
||||
public patchAssetProfile({
|
||||
assetClass,
|
||||
assetSubClass,
|
||||
comment,
|
||||
dataSource,
|
||||
name,
|
||||
scraperConfiguration,
|
||||
symbol,
|
||||
symbolMapping
|
||||
}: UniqueAsset & UpdateAssetProfileDto) {
|
||||
return this.http.patch<EnhancedSymbolProfile>(
|
||||
`/api/v1/admin/profile-data/${dataSource}/${symbol}`,
|
||||
{ comment, scraperConfiguration, symbolMapping }
|
||||
{
|
||||
assetClass,
|
||||
assetSubClass,
|
||||
comment,
|
||||
name,
|
||||
scraperConfiguration,
|
||||
symbolMapping
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -237,25 +246,6 @@ export class AdminService {
|
||||
return this.http.post<Tag>(`/api/v1/tag`, aTag);
|
||||
}
|
||||
|
||||
public putMarketData({
|
||||
dataSource,
|
||||
date,
|
||||
marketData,
|
||||
symbol
|
||||
}: {
|
||||
dataSource: DataSource;
|
||||
date: Date;
|
||||
marketData: UpdateMarketDataDto;
|
||||
symbol: string;
|
||||
}) {
|
||||
const url = `/api/v1/admin/market-data/${dataSource}/${symbol}/${format(
|
||||
date,
|
||||
DATE_FORMAT
|
||||
)}`;
|
||||
|
||||
return this.http.put<MarketData>(url, marketData);
|
||||
}
|
||||
|
||||
public putPlatform(aPlatform: UpdatePlatformDto) {
|
||||
return this.http.put<Platform>(
|
||||
`/api/v1/platform/${aPlatform.id}`,
|
||||
|
@ -386,14 +386,20 @@ export class DataService {
|
||||
|
||||
public fetchPortfolioPerformance({
|
||||
filters,
|
||||
range
|
||||
range,
|
||||
withExcludedAccounts = false
|
||||
}: {
|
||||
filters?: Filter[];
|
||||
range: DateRange;
|
||||
withExcludedAccounts?: boolean;
|
||||
}): Observable<PortfolioPerformanceResponse> {
|
||||
let params = this.buildFiltersAsQueryParams({ filters });
|
||||
params = params.append('range', range);
|
||||
|
||||
if (withExcludedAccounts) {
|
||||
params = params.append('withExcludedAccounts', withExcludedAccounts);
|
||||
}
|
||||
|
||||
return this.http
|
||||
.get<any>(`/api/v2/portfolio/performance`, {
|
||||
params
|
||||
|
@ -286,7 +286,7 @@ export class ImportActivitiesService {
|
||||
|
||||
for (const key of ImportActivitiesService.QUANTITY_KEYS) {
|
||||
if (isFinite(item[key])) {
|
||||
return item[key];
|
||||
return Math.abs(item[key]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -372,7 +372,7 @@ export class ImportActivitiesService {
|
||||
|
||||
for (const key of ImportActivitiesService.UNIT_PRICE_KEYS) {
|
||||
if (isFinite(item[key])) {
|
||||
return item[key];
|
||||
return Math.abs(item[key]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -46,12 +46,10 @@ export class WebAuthnService {
|
||||
switchMap((attOps) => {
|
||||
return startRegistration(attOps);
|
||||
}),
|
||||
switchMap((attResp) => {
|
||||
switchMap((credential) => {
|
||||
return this.http.post<AuthDeviceDto>(
|
||||
`/api/v1/auth/webauthn/verify-attestation`,
|
||||
{
|
||||
credential: attResp
|
||||
}
|
||||
{ credential }
|
||||
);
|
||||
}),
|
||||
tap((authDevice) =>
|
||||
@ -65,6 +63,7 @@ export class WebAuthnService {
|
||||
|
||||
public deregister() {
|
||||
const deviceId = this.getDeviceId();
|
||||
|
||||
return this.http
|
||||
.delete<AuthDeviceDto>(`/api/v1/auth-device/${deviceId}`)
|
||||
.pipe(
|
||||
@ -82,20 +81,21 @@ export class WebAuthnService {
|
||||
|
||||
public login() {
|
||||
const deviceId = this.getDeviceId();
|
||||
|
||||
return this.http
|
||||
.post<PublicKeyCredentialRequestOptionsJSON>(
|
||||
`/api/v1/auth/webauthn/generate-assertion-options`,
|
||||
{ deviceId }
|
||||
)
|
||||
.pipe(
|
||||
switchMap((requestOptionsJSON) =>
|
||||
startAuthentication(requestOptionsJSON, true)
|
||||
),
|
||||
switchMap((assertionResponse) => {
|
||||
switchMap((requestOptionsJSON) => {
|
||||
return startAuthentication(requestOptionsJSON);
|
||||
}),
|
||||
switchMap((credential) => {
|
||||
return this.http.post<{ authToken: string }>(
|
||||
`/api/v1/auth/webauthn/verify-assertion`,
|
||||
{
|
||||
credential: assertionResponse,
|
||||
credential,
|
||||
deviceId
|
||||
}
|
||||
);
|
||||
|
BIN
apps/client/src/assets/images/blog/hacktoberfest-2023-badges.png
Normal file
BIN
apps/client/src/assets/images/blog/hacktoberfest-2023-badges.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 263 KiB |
Binary file not shown.
After Width: | Height: | Size: 227 KiB |
@ -6,10 +6,7 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta content="yes" name="apple-mobile-web-app-capable" />
|
||||
<meta content="${description}" name="description" />
|
||||
<meta
|
||||
content="app, asset, cryptocurrency, dashboard, etf, finance, management, performance, portfolio, software, stock, trading, wealth, web3"
|
||||
name="keywords"
|
||||
/>
|
||||
<meta content="${keywords}" name="keywords" />
|
||||
<meta content="yes" name="mobile-web-app-capable" />
|
||||
<meta content="summary_large_image" name="twitter:card" />
|
||||
<meta
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -16,6 +16,10 @@ import { ghostfolioScraperApiSymbolPrefix, locale } from './config';
|
||||
import { Benchmark, UniqueAsset } from './interfaces';
|
||||
import { ColorScheme } from './types';
|
||||
|
||||
export const DATE_FORMAT = 'yyyy-MM-dd';
|
||||
export const DATE_FORMAT_MONTHLY = 'MMMM yyyy';
|
||||
export const DATE_FORMAT_YEARLY = 'yyyy';
|
||||
|
||||
const NUMERIC_REGEXP = /[-]{0,1}[\d]*[.,]{0,1}[\d]+/g;
|
||||
|
||||
export function capitalize(aString: string) {
|
||||
@ -240,10 +244,6 @@ export function groupBy<T, K extends keyof T>(
|
||||
return map;
|
||||
}
|
||||
|
||||
export function isCurrency(aSymbol = '') {
|
||||
return currencies[aSymbol];
|
||||
}
|
||||
|
||||
export function interpolate(template: string, context: any) {
|
||||
return template?.replace(/[$]{([^}]+)}/g, (_, objectPath) => {
|
||||
const properties = objectPath.split('.');
|
||||
@ -254,44 +254,10 @@ export function interpolate(template: string, context: any) {
|
||||
});
|
||||
}
|
||||
|
||||
export function resetHours(aDate: Date) {
|
||||
const year = getYear(aDate);
|
||||
const month = getMonth(aDate);
|
||||
const day = getDate(aDate);
|
||||
|
||||
return new Date(Date.UTC(year, month, day));
|
||||
export function isCurrency(aSymbol = '') {
|
||||
return currencies[aSymbol];
|
||||
}
|
||||
|
||||
export function resolveFearAndGreedIndex(aValue: number) {
|
||||
if (aValue <= 25) {
|
||||
return { emoji: '🥵', text: 'Extreme Fear' };
|
||||
} else if (aValue <= 45) {
|
||||
return { emoji: '😨', text: 'Fear' };
|
||||
} else if (aValue <= 55) {
|
||||
return { emoji: '😐', text: 'Neutral' };
|
||||
} else if (aValue < 75) {
|
||||
return { emoji: '😜', text: 'Greed' };
|
||||
} else {
|
||||
return { emoji: '🤪', text: 'Extreme Greed' };
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveMarketCondition(
|
||||
aMarketCondition: Benchmark['marketCondition']
|
||||
) {
|
||||
if (aMarketCondition === 'BEAR_MARKET') {
|
||||
return { emoji: '🐻' };
|
||||
} else if (aMarketCondition === 'BULL_MARKET') {
|
||||
return { emoji: '🐮' };
|
||||
} else {
|
||||
return { emoji: '⚪' };
|
||||
}
|
||||
}
|
||||
|
||||
export const DATE_FORMAT = 'yyyy-MM-dd';
|
||||
export const DATE_FORMAT_MONTHLY = 'MMMM yyyy';
|
||||
export const DATE_FORMAT_YEARLY = 'yyyy';
|
||||
|
||||
export function parseDate(date: string): Date | null {
|
||||
// Transform 'yyyyMMdd' format to supported format by parse function
|
||||
if (date?.length === 8) {
|
||||
@ -334,3 +300,37 @@ export function parseSymbol({ dataSource, symbol }: UniqueAsset) {
|
||||
export function prettifySymbol(aSymbol: string): string {
|
||||
return aSymbol?.replace(ghostfolioScraperApiSymbolPrefix, '');
|
||||
}
|
||||
|
||||
export function resetHours(aDate: Date) {
|
||||
const year = getYear(aDate);
|
||||
const month = getMonth(aDate);
|
||||
const day = getDate(aDate);
|
||||
|
||||
return new Date(Date.UTC(year, month, day));
|
||||
}
|
||||
|
||||
export function resolveFearAndGreedIndex(aValue: number) {
|
||||
if (aValue <= 25) {
|
||||
return { emoji: '🥵', key: 'EXTREME_FEAR', text: 'Extreme Fear' };
|
||||
} else if (aValue <= 45) {
|
||||
return { emoji: '😨', key: 'FEAR', text: 'Fear' };
|
||||
} else if (aValue <= 55) {
|
||||
return { emoji: '😐', key: 'NEUTRAL', text: 'Neutral' };
|
||||
} else if (aValue < 75) {
|
||||
return { emoji: '😜', key: 'GREED', text: 'Greed' };
|
||||
} else {
|
||||
return { emoji: '🤪', key: 'EXTREME_GREED', text: 'Extreme Greed' };
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveMarketCondition(
|
||||
aMarketCondition: Benchmark['marketCondition']
|
||||
) {
|
||||
if (aMarketCondition === 'BEAR_MARKET') {
|
||||
return { emoji: '🐻' };
|
||||
} else if (aMarketCondition === 'BULL_MARKET') {
|
||||
return { emoji: '🐮' };
|
||||
} else {
|
||||
return { emoji: '⚪' };
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,11 @@
|
||||
import { UniqueAsset } from './unique-asset.interface';
|
||||
|
||||
export interface AdminData {
|
||||
exchangeRates: { label1: string; label2: string; value: number }[];
|
||||
exchangeRates: ({
|
||||
label1: string;
|
||||
label2: string;
|
||||
value: number;
|
||||
} & UniqueAsset)[];
|
||||
settings: { [key: string]: boolean | object | string | string[] };
|
||||
transactionCount: number;
|
||||
userCount: number;
|
||||
|
4
libs/common/src/lib/interfaces/currency.interface.ts
Normal file
4
libs/common/src/lib/interfaces/currency.interface.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface Currency {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
export interface Product {
|
||||
alias?: string;
|
||||
component: any;
|
||||
founded?: number;
|
||||
hasFreePlan?: boolean;
|
||||
|
@ -30,8 +30,10 @@
|
||||
[disabled]="dataSource.data.length === 0"
|
||||
(click)="onImportDividends()"
|
||||
>
|
||||
<ion-icon class="mr-2" name="color-wand-outline"></ion-icon>
|
||||
<ng-container i18n>Import Dividends</ng-container>...
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="color-wand-outline"></ion-icon>
|
||||
<ng-container i18n>Import Dividends</ng-container>...
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="hasPermissionToExportActivities"
|
||||
@ -40,8 +42,10 @@
|
||||
[disabled]="dataSource.data.length === 0"
|
||||
(click)="onExport()"
|
||||
>
|
||||
<ion-icon class="mr-2" name="cloud-download-outline"></ion-icon>
|
||||
<span i18n>Export Activities</span>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="cloud-download-outline"></ion-icon>
|
||||
<span i18n>Export Activities</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="hasPermissionToExportActivities"
|
||||
@ -50,16 +54,20 @@
|
||||
[disabled]="!hasDrafts"
|
||||
(click)="onExportDrafts()"
|
||||
>
|
||||
<ion-icon class="mr-2" name="calendar-clear-outline"></ion-icon>
|
||||
<span i18n>Export Drafts as ICS</span>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="calendar-clear-outline"></ion-icon>
|
||||
<span i18n>Export Drafts as ICS</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="align-items-center d-flex"
|
||||
mat-menu-item
|
||||
(click)="onDeleteAllActivities()"
|
||||
>
|
||||
<ion-icon class="mr-2" name="trash-outline"></ion-icon>
|
||||
<span i18n>Delete all Activities</span>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="trash-outline"></ion-icon>
|
||||
<span i18n>Delete all Activities</span>
|
||||
</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
@ -440,8 +448,10 @@
|
||||
mat-menu-item
|
||||
(click)="onImport()"
|
||||
>
|
||||
<ion-icon class="mr-2" name="cloud-upload-outline"></ion-icon>
|
||||
<ng-container i18n>Import Activities</ng-container>...
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="cloud-upload-outline"></ion-icon>
|
||||
<ng-container i18n>Import Activities</ng-container>...
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="hasPermissionToCreateActivity"
|
||||
@ -449,8 +459,10 @@
|
||||
[disabled]="dataSource.data.length === 0"
|
||||
(click)="onImportDividends()"
|
||||
>
|
||||
<ion-icon class="mr-2" name="color-wand-outline"></ion-icon>
|
||||
<ng-container i18n>Import Dividends</ng-container>...
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="color-wand-outline"></ion-icon>
|
||||
<ng-container i18n>Import Dividends</ng-container>...
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="hasPermissionToExportActivities"
|
||||
@ -459,8 +471,10 @@
|
||||
[disabled]="dataSource.data.length === 0"
|
||||
(click)="onExport()"
|
||||
>
|
||||
<ion-icon class="mr-2" name="cloud-download-outline"></ion-icon>
|
||||
<span i18n>Export Activities</span>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="cloud-download-outline"></ion-icon>
|
||||
<span i18n>Export Activities</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="hasPermissionToExportActivities"
|
||||
@ -469,8 +483,10 @@
|
||||
[disabled]="!hasDrafts"
|
||||
(click)="onExportDrafts()"
|
||||
>
|
||||
<ion-icon class="mr-2" name="calendar-clear-outline"></ion-icon>
|
||||
<span i18n>Export Drafts as ICS</span>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="calendar-clear-outline"></ion-icon>
|
||||
<span i18n>Export Drafts as ICS</span>
|
||||
</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</th>
|
||||
@ -486,24 +502,32 @@
|
||||
</button>
|
||||
<mat-menu #activityMenu="matMenu" xPosition="before">
|
||||
<button mat-menu-item (click)="onUpdateActivity(element)">
|
||||
<ion-icon class="mr-2" name="create-outline"></ion-icon>
|
||||
<span i18n>Edit</span>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="create-outline"></ion-icon>
|
||||
<span i18n>Edit</span>
|
||||
</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onCloneActivity(element)">
|
||||
<ion-icon class="mr-2" name="copy-outline"></ion-icon>
|
||||
<span i18n>Clone</span>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="copy-outline"></ion-icon>
|
||||
<span i18n>Clone</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
[disabled]="!element.isDraft"
|
||||
(click)="onExportDraft(element.id)"
|
||||
>
|
||||
<ion-icon class="mr-2" name="calendar-clear-outline"></ion-icon>
|
||||
<span i18n>Export Draft as ICS</span>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="calendar-clear-outline"></ion-icon>
|
||||
<span i18n>Export Draft as ICS</span>
|
||||
</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onDeleteActivity(element.id)">
|
||||
<ion-icon class="mr-2" name="trash-outline"></ion-icon>
|
||||
<span i18n>Delete</span>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="trash-outline"></ion-icon>
|
||||
<span i18n>Delete</span>
|
||||
</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
|
@ -0,0 +1,21 @@
|
||||
<input
|
||||
autocapitalize="off"
|
||||
autocomplete="off"
|
||||
matInput
|
||||
[formControl]="control"
|
||||
[matAutocomplete]="currencyAutocomplete"
|
||||
/>
|
||||
|
||||
<mat-autocomplete
|
||||
#currencyAutocomplete="matAutocomplete"
|
||||
[displayWith]="displayFn"
|
||||
(optionSelected)="onUpdateCurrency($event)"
|
||||
>
|
||||
<mat-option
|
||||
*ngFor="let currencyItem of filteredCurrencies"
|
||||
class="line-height-1"
|
||||
[value]="currencyItem"
|
||||
>
|
||||
{{ currencyItem.label }}
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
@ -0,0 +1,3 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
167
libs/ui/src/lib/currency-selector/currency-selector.component.ts
Normal file
167
libs/ui/src/lib/currency-selector/currency-selector.component.ts
Normal file
@ -0,0 +1,167 @@
|
||||
import { FocusMonitor } from '@angular/cdk/a11y';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { FormControl, FormGroupDirective, NgControl } from '@angular/forms';
|
||||
import {
|
||||
MatAutocomplete,
|
||||
MatAutocompleteSelectedEvent
|
||||
} from '@angular/material/autocomplete';
|
||||
import { MatFormFieldControl } from '@angular/material/form-field';
|
||||
import { MatInput } from '@angular/material/input';
|
||||
import { Currency } from '@ghostfolio/common/interfaces/currency.interface';
|
||||
import { AbstractMatFormField } from '@ghostfolio/ui/shared/abstract-mat-form-field';
|
||||
import { Subject } from 'rxjs';
|
||||
import { map, startWith, takeUntil } from 'rxjs/operators';
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: {
|
||||
'[attr.aria-describedBy]': 'describedBy',
|
||||
'[id]': 'id'
|
||||
},
|
||||
providers: [
|
||||
{
|
||||
provide: MatFormFieldControl,
|
||||
useExisting: CurrencySelectorComponent
|
||||
}
|
||||
],
|
||||
selector: 'gf-currency-selector',
|
||||
styleUrls: ['./currency-selector.component.scss'],
|
||||
templateUrl: 'currency-selector.component.html'
|
||||
})
|
||||
export class CurrencySelectorComponent
|
||||
extends AbstractMatFormField<Currency>
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
@Input() private currencies: Currency[] = [];
|
||||
@Input() private formControlName: string;
|
||||
|
||||
@ViewChild(MatInput) private input: MatInput;
|
||||
|
||||
@ViewChild('currencyAutocomplete')
|
||||
public currencyAutocomplete: MatAutocomplete;
|
||||
|
||||
public control = new FormControl();
|
||||
public filteredCurrencies: Currency[] = [];
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor(
|
||||
public readonly _elementRef: ElementRef,
|
||||
public readonly _focusMonitor: FocusMonitor,
|
||||
public readonly changeDetectorRef: ChangeDetectorRef,
|
||||
private readonly formGroupDirective: FormGroupDirective,
|
||||
public readonly ngControl: NgControl
|
||||
) {
|
||||
super(_elementRef, _focusMonitor, ngControl);
|
||||
|
||||
this.controlType = 'currency-selector';
|
||||
}
|
||||
|
||||
public ngOnInit() {
|
||||
if (this.disabled) {
|
||||
this.control.disable();
|
||||
}
|
||||
|
||||
const formGroup = this.formGroupDirective.form;
|
||||
|
||||
if (formGroup) {
|
||||
const control = formGroup.get(this.formControlName);
|
||||
|
||||
if (control) {
|
||||
this.value = this.currencies.find(({ value }) => {
|
||||
return value === control.value;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.control.valueChanges
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
if (super.value?.value) {
|
||||
super.value.value = null;
|
||||
}
|
||||
});
|
||||
|
||||
this.control.valueChanges
|
||||
.pipe(
|
||||
takeUntil(this.unsubscribeSubject),
|
||||
startWith(''),
|
||||
map((value) => {
|
||||
return value ? this.filter(value) : this.currencies.slice();
|
||||
})
|
||||
)
|
||||
.subscribe((values) => {
|
||||
this.filteredCurrencies = values;
|
||||
});
|
||||
}
|
||||
|
||||
public displayFn(currency: Currency) {
|
||||
return currency?.label ?? '';
|
||||
}
|
||||
|
||||
public get empty() {
|
||||
return this.input?.empty;
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this.input.focus();
|
||||
}
|
||||
|
||||
public ngDoCheck() {
|
||||
if (this.ngControl) {
|
||||
this.validateRequired();
|
||||
this.errorState = this.ngControl.invalid && this.ngControl.touched;
|
||||
this.stateChanges.next();
|
||||
}
|
||||
}
|
||||
|
||||
public onUpdateCurrency(event: MatAutocompleteSelectedEvent) {
|
||||
super.value = {
|
||||
label: event.option.value.label,
|
||||
value: event.option.value.value
|
||||
} as Currency;
|
||||
}
|
||||
|
||||
public set value(value: Currency) {
|
||||
const newValue =
|
||||
typeof value === 'object' && value !== null ? { ...value } : value;
|
||||
this.control.setValue(newValue);
|
||||
super.value = newValue;
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
super.ngOnDestroy();
|
||||
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
private filter(value: Currency | string) {
|
||||
const filterValue =
|
||||
typeof value === 'string'
|
||||
? value?.toLowerCase()
|
||||
: value?.value.toLowerCase();
|
||||
|
||||
return this.currencies.filter((currency) => {
|
||||
return currency.value.toLowerCase().startsWith(filterValue);
|
||||
});
|
||||
}
|
||||
|
||||
private validateRequired() {
|
||||
const requiredCheck = super.required
|
||||
? !super.value.label || !super.value.value
|
||||
: false;
|
||||
|
||||
if (requiredCheck) {
|
||||
this.ngControl.control.setErrors({ invalidData: true });
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user