Compare commits
73 Commits
Author | SHA1 | Date | |
---|---|---|---|
423bd92b89 | |||
5dc331e386 | |||
744dc51dcd | |||
b0c53d050a | |||
830569b38e | |||
35b4aef06f | |||
bc2fd9c970 | |||
c42a8aebed | |||
fad1adb91b | |||
9cd37f8de0 | |||
d49b90d7a5 | |||
130a9ea062 | |||
ffc6309850 | |||
976cc7f243 | |||
7067aca04b | |||
1c9805bb96 | |||
8227a2d91a | |||
194aee97db | |||
0f77169952 | |||
0f8dc62c53 | |||
554136cdcd | |||
83b5cfff1f | |||
dcec3accf0 | |||
f08b0b570b | |||
8386fec98a | |||
4d3dff3e5b | |||
76890e63fa | |||
4fb2aebf4f | |||
ed5cd3b978 | |||
469c1936b4 | |||
8b3cc5c11a | |||
ee086638f3 | |||
58d1abbd38 | |||
ba979cbae2 | |||
8cda43bb63 | |||
c4499df74c | |||
24bcc15b6a | |||
ff121243e4 | |||
70e633b997 | |||
0780ee4adb | |||
09613f9324 | |||
8642b1a7af | |||
f96f861341 | |||
a201fc7a97 | |||
a97110348c | |||
a25d5b9dc0 | |||
6c2acf2aa6 | |||
519827045a | |||
79a7e12a9f | |||
bf20a5de82 | |||
0adefe14e1 | |||
f24561cc3d | |||
873fd53715 | |||
e5d8faf2dc | |||
65d3bd2802 | |||
ad60373813 | |||
b725e6e2ec | |||
88c420ca5e | |||
118e17f78c | |||
cc92592d86 | |||
46eb3254a9 | |||
2477491f18 | |||
5fc9fde129 | |||
00e50c6abe | |||
8131a7ad03 | |||
f5e6f7dcfe | |||
87501e094d | |||
d3bfdf78c3 | |||
fc4e6ae6db | |||
23e4d5454d | |||
fdcf5fd396 | |||
8a9ae9bb33 | |||
3fb7e746df |
@ -24,12 +24,18 @@
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"extends": ["plugin:@nx/typescript"],
|
||||
"rules": {}
|
||||
"rules": {
|
||||
"@typescript-eslint/no-extra-semi": "error",
|
||||
"no-extra-semi": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.js", "*.jsx"],
|
||||
"extends": ["plugin:@nx/javascript"],
|
||||
"rules": {}
|
||||
"rules": {
|
||||
"@typescript-eslint/no-extra-semi": "error",
|
||||
"no-extra-semi": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.ts"],
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -28,6 +28,7 @@
|
||||
.env
|
||||
.env.prod
|
||||
.nx/cache
|
||||
.nx/workspace-data
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
|
@ -1,4 +1,5 @@
|
||||
/.nx/cache
|
||||
/.nx/workspace-data
|
||||
/apps/client/src/polyfills.ts
|
||||
/dist
|
||||
/test/import
|
||||
|
131
CHANGELOG.md
131
CHANGELOG.md
@ -5,6 +5,137 @@ 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.94.0 - 2024-07-09
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the language localization for German (`de`)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed a pagination issue in the activities endpoint by adding `id` as a secondary sort criterion to `date` to ensure consistent ordering
|
||||
|
||||
## 2.93.0 - 2024-07-07
|
||||
|
||||
### Added
|
||||
|
||||
- Added the _Crypto Coins Heatmap_ to the resources section
|
||||
- Added the _Stock Heatmap_ to the resources section
|
||||
- Extended the content of the _Self-Hosting_ section by the platforms concept on the Frequently Asked Questions (FAQ) page
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the allocations by ETF holding on the allocations page for the impersonation mode (experimental)
|
||||
- Improved the detection of REST APIs (`JSON`) used via the scraper configuration
|
||||
- Improved the usability to delete an asset profile of type currency in the historical market data table and the asset profile details dialog of the admin control
|
||||
- Refreshed the cryptocurrencies list
|
||||
- Refactored the thresholds of the rules in the _X-ray_ section
|
||||
- Removed the obsolete `version` from the `docker-compose` files
|
||||
- Upgraded `Nx` from version `19.2.2` to `19.4.0`
|
||||
|
||||
## 2.92.0 - 2024-06-30
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for bulk deletion of asset profiles from the market data table in the admin control panel
|
||||
|
||||
### Changed
|
||||
|
||||
- Added support for derived currencies in the currency validation
|
||||
- Added support for automatic deletion of unused asset profiles when deleting activities
|
||||
- Improved the caching of the benchmarks in the markets overview (only cache if needed)
|
||||
- Upgraded `prisma` from version `5.15.0` to `5.16.1`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue with the all time high in the benchmarks of the markets overview
|
||||
|
||||
## 2.91.0 - 2024-06-26
|
||||
|
||||
### Added
|
||||
|
||||
- Added a benchmarks preset to the historical market data table of the admin control panel
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgraded `angular` from version `18.0.2` to `18.0.4`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the dialog position (center) on mobile
|
||||
- Fixed the horizontal overflow in the historical market data table of the admin control panel
|
||||
- Changed the mechanism of the `INTRADAY` data gathering to persist data only if the market state is `OPEN`
|
||||
- Fixed the creation of activities with `MANUAL` data source (with no historical market data)
|
||||
|
||||
## 2.90.0 - 2024-06-22
|
||||
|
||||
### Added
|
||||
|
||||
- Added a dialog for the benchmarks in the markets overview
|
||||
- Extended the asset profile details dialog of the admin control for currencies
|
||||
- Extended the content of the _Self-Hosting_ section by the mobile app question on the Frequently Asked Questions (FAQ) page
|
||||
|
||||
### Changed
|
||||
|
||||
- Moved the indicator for active filters from experimental to general availability
|
||||
- Improved the error handling in the biometric authentication registration
|
||||
- Improved the language localization for German (`de`)
|
||||
- Set up SSL for local development
|
||||
- Upgraded the _Stripe_ dependencies
|
||||
- Upgraded `marked` from version `9.1.6` to `13.0.0`
|
||||
- Upgraded `ngx-device-detector` from version `5.0.1` to `8.0.0`
|
||||
- Upgraded `ngx-markdown` from version `17.1.1` to `18.0.0`
|
||||
- Upgraded `zone.js` from version `0.14.5` to `0.14.7`
|
||||
|
||||
## 2.89.0 - 2024-06-14
|
||||
|
||||
### Added
|
||||
|
||||
- Extended the historical market data table with currencies preset by date and activities count in the admin control panel
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the date validation in the create, import and update activities endpoints
|
||||
- Improved the language localization for German (`de`)
|
||||
|
||||
## 2.88.0 - 2024-06-11
|
||||
|
||||
### Added
|
||||
|
||||
- Set the image source label in `Dockerfile`
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the style of the blog post list
|
||||
- Migrated the `@ghostfolio/client` components to control flow
|
||||
- Improved the language localization for German (`de`)
|
||||
- Upgraded `angular` from version `17.3.10` to `18.0.2`
|
||||
- Upgraded `Nx` from version `19.0.5` to `19.2.2`
|
||||
|
||||
## 2.87.0 - 2024-06-08
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the portfolio summary
|
||||
- Improved the allocations by ETF holding on the allocations page (experimental)
|
||||
- Improved the error handling in the `HttpResponseInterceptor`
|
||||
- Improved the language localization for German (`de`)
|
||||
- Upgraded `prisma` from version `5.14.0` to `5.15.0`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue in the _FIRE_ calculator
|
||||
|
||||
## 2.86.0 - 2024-06-07
|
||||
|
||||
### Added
|
||||
|
||||
- Introduced the allocations by ETF holding on the allocations page (experimental)
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgraded `prettier` from version `3.2.5` to `3.3.1`
|
||||
|
||||
## 2.85.0 - 2024-06-06
|
||||
|
||||
### Added
|
||||
|
@ -10,7 +10,7 @@ Remove permission in `UserService` using `without()`
|
||||
|
||||
### Frontend
|
||||
|
||||
Use `*ngIf="user?.settings?.isExperimentalFeatures"` in HTML template
|
||||
Use `@if (user?.settings?.isExperimentalFeatures) {}` in HTML template
|
||||
|
||||
## Git
|
||||
|
||||
|
@ -51,6 +51,9 @@ RUN yarn database:generate-typings
|
||||
|
||||
# Image to run, copy everything needed from builder
|
||||
FROM node:18-slim
|
||||
|
||||
LABEL org.opencontainers.image.source="https://github.com/ghostfolio/ghostfolio"
|
||||
|
||||
RUN apt update && apt install -y \
|
||||
curl \
|
||||
openssl \
|
||||
|
12
README.md
12
README.md
@ -7,7 +7,7 @@
|
||||
**Open Source Wealth Management Software**
|
||||
|
||||
[**Ghostfol.io**](https://ghostfol.io) | [**Live Demo**](https://ghostfol.io/en/demo) | [**Ghostfolio Premium**](https://ghostfol.io/en/pricing) | [**FAQ**](https://ghostfol.io/en/faq) |
|
||||
[**Blog**](https://ghostfol.io/en/blog) | [**Slack**](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg) | [**X**](https://twitter.com/ghostfolio_)
|
||||
[**Blog**](https://ghostfol.io/en/blog) | [**Slack**](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg) | [**X**](https://x.com/ghostfolio_)
|
||||
|
||||
[](https://www.buymeacoffee.com/ghostfolio)
|
||||
[](#contributing)
|
||||
@ -47,7 +47,7 @@ Ghostfolio is for you if you are...
|
||||
|
||||
- ✅ Create, update and delete transactions
|
||||
- ✅ Multi account management
|
||||
- ✅ Portfolio performance: Time-weighted rate of return (TWR) for `Today`, `YTD`, `1Y`, `5Y`, `Max`
|
||||
- ✅ Portfolio performance: Time-weighted rate of return (TWR) for `Today`, `WTD`, `MTD`, `YTD`, `1Y`, `5Y`, `Max`
|
||||
- ✅ Various charts
|
||||
- ✅ Static analysis to identify potential risks in your portfolio
|
||||
- ✅ Import and export transactions
|
||||
@ -89,7 +89,7 @@ We provide official container images hosted on [Docker Hub](https://hub.docker.c
|
||||
| ------------------------ | ------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `ACCESS_TOKEN_SALT` | string | | A random string used as salt for access tokens |
|
||||
| `API_KEY_COINGECKO_DEMO` | string (`optional`) | | The _CoinGecko_ Demo API key |
|
||||
| `API_KEY_COINGECKO_PRO` | string (`optional`) | | The _CoinGecko_ Pro API |
|
||||
| `API_KEY_COINGECKO_PRO` | string (`optional`) | | The _CoinGecko_ Pro API key |
|
||||
| `DATABASE_URL` | string | | The database connection URL, e.g. `postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?sslmode=prefer` |
|
||||
| `HOST` | string (`optional`) | `0.0.0.0` | The host where the Ghostfolio application will run on |
|
||||
| `JWT_SECRET_KEY` | string | | A random string used for _JSON Web Tokens_ (JWT) |
|
||||
@ -161,7 +161,7 @@ Ghostfolio is available for various home server systems, including [CasaOS](http
|
||||
1. Run `yarn database:setup` to initialize the database schema
|
||||
1. Run `git config core.hooksPath ./git-hooks/` to setup git hooks
|
||||
1. Start the server and the client (see [_Development_](#Development))
|
||||
1. Open http://localhost:4200/en in your browser
|
||||
1. Open https://localhost:4200/en in your browser
|
||||
1. Create a new user via _Get Started_ (this first user will get the role `ADMIN`)
|
||||
|
||||
### Start Server
|
||||
@ -176,7 +176,7 @@ Run `yarn start:server`
|
||||
|
||||
### Start Client
|
||||
|
||||
Run `yarn start:client` and open http://localhost:4200/en in your browser
|
||||
Run `yarn start:client` and open https://localhost:4200/en in your browser
|
||||
|
||||
### Start _Storybook_
|
||||
|
||||
@ -275,7 +275,7 @@ Are you building your own project? Add the `ghostfolio` topic to your _GitHub_ r
|
||||
|
||||
Ghostfolio is **100% free** and **open source**. We encourage and support an active and healthy community that accepts contributions from the public - including you.
|
||||
|
||||
Not sure what to work on? We have [some ideas](https://github.com/ghostfolio/ghostfolio/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22), even for [newcomers](https://github.com/ghostfolio/ghostfolio/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). Please join the Ghostfolio [Slack](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg) channel or post to [@ghostfolio\_](https://twitter.com/ghostfolio_) on _X_. We would love to hear from you.
|
||||
Not sure what to work on? We have [some ideas](https://github.com/ghostfolio/ghostfolio/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22), even for [newcomers](https://github.com/ghostfolio/ghostfolio/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). Please join the Ghostfolio [Slack](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg) channel or post to [@ghostfolio\_](https://x.com/ghostfolio_) on _X_. We would love to hear from you.
|
||||
|
||||
If you like to support this project, get [**Ghostfolio Premium**](https://ghostfol.io/en/pricing) or [**Buy me a coffee**](https://www.buymeacoffee.com/ghostfolio).
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { IsCurrencyCode } from '@ghostfolio/api/validators/is-currency-code';
|
||||
|
||||
import { Transform, TransformFnParams } from 'class-transformer';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsISO4217CurrencyCode,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
@ -20,7 +21,7 @@ export class CreateAccountDto {
|
||||
)
|
||||
comment?: string;
|
||||
|
||||
@IsISO4217CurrencyCode()
|
||||
@IsCurrencyCode()
|
||||
currency: string;
|
||||
|
||||
@IsOptional()
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { IsCurrencyCode } from '@ghostfolio/api/validators/is-currency-code';
|
||||
|
||||
import { Transform, TransformFnParams } from 'class-transformer';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsISO4217CurrencyCode,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
@ -20,7 +21,7 @@ export class UpdateAccountDto {
|
||||
)
|
||||
comment?: string;
|
||||
|
||||
@IsISO4217CurrencyCode()
|
||||
@IsCurrencyCode()
|
||||
currency: string;
|
||||
|
||||
@IsString()
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { BenchmarkModule } from '@ghostfolio/api/app/benchmark/benchmark.module';
|
||||
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
|
||||
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
|
||||
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
|
||||
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
|
||||
@ -19,11 +21,13 @@ import { QueueModule } from './queue/queue.module';
|
||||
@Module({
|
||||
imports: [
|
||||
ApiModule,
|
||||
BenchmarkModule,
|
||||
ConfigurationModule,
|
||||
DataGatheringModule,
|
||||
DataProviderModule,
|
||||
ExchangeRateDataModule,
|
||||
MarketDataModule,
|
||||
OrderModule,
|
||||
PrismaModule,
|
||||
PropertyModule,
|
||||
QueueModule,
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { BenchmarkService } from '@ghostfolio/api/app/benchmark/benchmark.service';
|
||||
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
||||
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
|
||||
import { environment } from '@ghostfolio/api/environments/environment';
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||
@ -13,11 +15,13 @@ import {
|
||||
PROPERTY_IS_READ_ONLY_MODE,
|
||||
PROPERTY_IS_USER_SIGNUP_ENABLED
|
||||
} from '@ghostfolio/common/config';
|
||||
import { isCurrency, getCurrencyFromSymbol } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
AdminData,
|
||||
AdminMarketData,
|
||||
AdminMarketDataDetails,
|
||||
AdminMarketDataItem,
|
||||
EnhancedSymbolProfile,
|
||||
Filter,
|
||||
UniqueAsset
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
@ -38,10 +42,12 @@ import { groupBy } from 'lodash';
|
||||
@Injectable()
|
||||
export class AdminService {
|
||||
public constructor(
|
||||
private readonly benchmarkService: BenchmarkService,
|
||||
private readonly configurationService: ConfigurationService,
|
||||
private readonly dataProviderService: DataProviderService,
|
||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||
private readonly marketDataService: MarketDataService,
|
||||
private readonly orderService: OrderService,
|
||||
private readonly prismaService: PrismaService,
|
||||
private readonly propertyService: PropertyService,
|
||||
private readonly subscriptionService: SubscriptionService,
|
||||
@ -147,7 +153,16 @@ export class AdminService {
|
||||
[{ symbol: 'asc' }];
|
||||
const where: Prisma.SymbolProfileWhereInput = {};
|
||||
|
||||
if (presetId === 'CURRENCIES') {
|
||||
if (presetId === 'BENCHMARKS') {
|
||||
const benchmarkAssetProfiles =
|
||||
await this.benchmarkService.getBenchmarkAssetProfiles();
|
||||
|
||||
where.id = {
|
||||
in: benchmarkAssetProfiles.map(({ id }) => {
|
||||
return id;
|
||||
})
|
||||
};
|
||||
} else if (presetId === 'CURRENCIES') {
|
||||
return this.getMarketDataForCurrencies();
|
||||
} else if (
|
||||
presetId === 'ETF_WITHOUT_COUNTRIES' ||
|
||||
@ -295,6 +310,16 @@ export class AdminService {
|
||||
dataSource,
|
||||
symbol
|
||||
}: UniqueAsset): Promise<AdminMarketDataDetails> {
|
||||
let activitiesCount: EnhancedSymbolProfile['activitiesCount'] = 0;
|
||||
let currency: EnhancedSymbolProfile['currency'] = '-';
|
||||
let dateOfFirstActivity: EnhancedSymbolProfile['dateOfFirstActivity'];
|
||||
|
||||
if (isCurrency(getCurrencyFromSymbol(symbol))) {
|
||||
currency = getCurrencyFromSymbol(symbol);
|
||||
({ activitiesCount, dateOfFirstActivity } =
|
||||
await this.orderService.getStatisticsByCurrency(currency));
|
||||
}
|
||||
|
||||
const [[assetProfile], marketData] = await Promise.all([
|
||||
this.symbolProfileService.getSymbolProfiles([
|
||||
{
|
||||
@ -322,8 +347,11 @@ export class AdminService {
|
||||
return {
|
||||
marketData,
|
||||
assetProfile: assetProfile ?? {
|
||||
symbol,
|
||||
currency: '-'
|
||||
activitiesCount,
|
||||
currency,
|
||||
dataSource,
|
||||
dateOfFirstActivity,
|
||||
symbol
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -335,6 +363,7 @@ export class AdminService {
|
||||
countries,
|
||||
currency,
|
||||
dataSource,
|
||||
holdings,
|
||||
name,
|
||||
scraperConfiguration,
|
||||
sectors,
|
||||
@ -355,6 +384,7 @@ export class AdminService {
|
||||
countries,
|
||||
currency,
|
||||
dataSource,
|
||||
holdings,
|
||||
scraperConfiguration,
|
||||
sectors,
|
||||
symbol,
|
||||
@ -407,30 +437,45 @@ export class AdminService {
|
||||
by: ['dataSource', 'symbol']
|
||||
});
|
||||
|
||||
const marketData: AdminMarketDataItem[] = this.exchangeRateDataService
|
||||
.getCurrencyPairs()
|
||||
.map(({ dataSource, symbol }) => {
|
||||
const marketDataItemCount =
|
||||
marketDataItems.find((marketDataItem) => {
|
||||
return (
|
||||
marketDataItem.dataSource === dataSource &&
|
||||
marketDataItem.symbol === symbol
|
||||
);
|
||||
})?._count ?? 0;
|
||||
const marketDataPromise: Promise<AdminMarketDataItem>[] =
|
||||
this.exchangeRateDataService
|
||||
.getCurrencyPairs()
|
||||
.map(async ({ dataSource, symbol }) => {
|
||||
let activitiesCount: EnhancedSymbolProfile['activitiesCount'] = 0;
|
||||
let currency: EnhancedSymbolProfile['currency'] = '-';
|
||||
let dateOfFirstActivity: EnhancedSymbolProfile['dateOfFirstActivity'];
|
||||
|
||||
return {
|
||||
dataSource,
|
||||
marketDataItemCount,
|
||||
symbol,
|
||||
assetClass: AssetClass.LIQUIDITY,
|
||||
countriesCount: 0,
|
||||
currency: symbol.replace(DEFAULT_CURRENCY, ''),
|
||||
id: undefined,
|
||||
name: symbol,
|
||||
sectorsCount: 0
|
||||
};
|
||||
});
|
||||
if (isCurrency(getCurrencyFromSymbol(symbol))) {
|
||||
currency = getCurrencyFromSymbol(symbol);
|
||||
({ activitiesCount, dateOfFirstActivity } =
|
||||
await this.orderService.getStatisticsByCurrency(currency));
|
||||
}
|
||||
|
||||
const marketDataItemCount =
|
||||
marketDataItems.find((marketDataItem) => {
|
||||
return (
|
||||
marketDataItem.dataSource === dataSource &&
|
||||
marketDataItem.symbol === symbol
|
||||
);
|
||||
})?._count ?? 0;
|
||||
|
||||
return {
|
||||
activitiesCount,
|
||||
currency,
|
||||
dataSource,
|
||||
marketDataItemCount,
|
||||
symbol,
|
||||
assetClass: AssetClass.LIQUIDITY,
|
||||
assetSubClass: AssetSubClass.CASH,
|
||||
countriesCount: 0,
|
||||
date: dateOfFirstActivity,
|
||||
id: undefined,
|
||||
name: symbol,
|
||||
sectorsCount: 0
|
||||
};
|
||||
});
|
||||
|
||||
const marketData = await Promise.all(marketDataPromise);
|
||||
return { marketData, count: marketData.length };
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { IsCurrencyCode } from '@ghostfolio/api/validators/is-currency-code';
|
||||
|
||||
import { AssetClass, AssetSubClass, Prisma } from '@prisma/client';
|
||||
import {
|
||||
IsArray,
|
||||
IsEnum,
|
||||
IsISO4217CurrencyCode,
|
||||
IsObject,
|
||||
IsOptional,
|
||||
IsString,
|
||||
@ -26,7 +27,7 @@ export class UpdateAssetProfileDto {
|
||||
@IsOptional()
|
||||
countries?: Prisma.InputJsonArray;
|
||||
|
||||
@IsISO4217CurrencyCode()
|
||||
@IsCurrencyCode()
|
||||
@IsOptional()
|
||||
currency?: string;
|
||||
|
||||
|
@ -25,6 +25,7 @@ import { AccessModule } from './access/access.module';
|
||||
import { AccountModule } from './account/account.module';
|
||||
import { AdminModule } from './admin/admin.module';
|
||||
import { AppController } from './app.controller';
|
||||
import { AssetModule } from './asset/asset.module';
|
||||
import { AuthDeviceModule } from './auth-device/auth-device.module';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { BenchmarkModule } from './benchmark/benchmark.module';
|
||||
@ -51,6 +52,7 @@ import { UserModule } from './user/user.module';
|
||||
AdminModule,
|
||||
AccessModule,
|
||||
AccountModule,
|
||||
AssetModule,
|
||||
AuthDeviceModule,
|
||||
AuthModule,
|
||||
BenchmarkModule,
|
||||
|
29
apps/api/src/app/asset/asset.controller.ts
Normal file
29
apps/api/src/app/asset/asset.controller.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { AdminService } from '@ghostfolio/api/app/admin/admin.service';
|
||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
|
||||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
|
||||
import type { AdminMarketDataDetails } from '@ghostfolio/common/interfaces';
|
||||
|
||||
import { Controller, Get, Param, UseInterceptors } from '@nestjs/common';
|
||||
import { DataSource } from '@prisma/client';
|
||||
import { pick } from 'lodash';
|
||||
|
||||
@Controller('asset')
|
||||
export class AssetController {
|
||||
public constructor(private readonly adminService: AdminService) {}
|
||||
|
||||
@Get(':dataSource/:symbol')
|
||||
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||
public async getAsset(
|
||||
@Param('dataSource') dataSource: DataSource,
|
||||
@Param('symbol') symbol: string
|
||||
): Promise<AdminMarketDataDetails> {
|
||||
const { assetProfile, marketData } =
|
||||
await this.adminService.getMarketDataBySymbol({ dataSource, symbol });
|
||||
|
||||
return {
|
||||
marketData,
|
||||
assetProfile: pick(assetProfile, ['dataSource', 'name', 'symbol'])
|
||||
};
|
||||
}
|
||||
}
|
17
apps/api/src/app/asset/asset.module.ts
Normal file
17
apps/api/src/app/asset/asset.module.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { AdminModule } from '@ghostfolio/api/app/admin/admin.module';
|
||||
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
|
||||
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { AssetController } from './asset.controller';
|
||||
|
||||
@Module({
|
||||
controllers: [AssetController],
|
||||
imports: [
|
||||
AdminModule,
|
||||
TransformDataSourceInRequestModule,
|
||||
TransformDataSourceInResponseModule
|
||||
]
|
||||
})
|
||||
export class AssetModule {}
|
@ -105,7 +105,7 @@ export class BenchmarkController {
|
||||
@Get(':dataSource/:symbol/:startDateString')
|
||||
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
||||
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||
public async getBenchmarkMarketDataBySymbol(
|
||||
public async getBenchmarkMarketDataForUser(
|
||||
@Param('dataSource') dataSource: DataSource,
|
||||
@Param('startDateString') startDateString: string,
|
||||
@Param('symbol') symbol: string,
|
||||
@ -117,7 +117,7 @@ export class BenchmarkController {
|
||||
);
|
||||
const userCurrency = this.request.user.Settings.settings.baseCurrency;
|
||||
|
||||
return this.benchmarkService.getMarketDataBySymbol({
|
||||
return this.benchmarkService.getMarketDataForUser({
|
||||
dataSource,
|
||||
endDate,
|
||||
startDate,
|
||||
|
@ -135,7 +135,7 @@ export class BenchmarkService {
|
||||
Promise.all(promisesAllTimeHighs),
|
||||
Promise.all(promisesBenchmarkTrends)
|
||||
]);
|
||||
let storeInCache = true;
|
||||
let storeInCache = useCache;
|
||||
|
||||
benchmarks = allTimeHighs.map((allTimeHigh, index) => {
|
||||
const { marketPrice } =
|
||||
@ -153,6 +153,7 @@ export class BenchmarkService {
|
||||
}
|
||||
|
||||
return {
|
||||
dataSource: benchmarkAssetProfiles[index].dataSource,
|
||||
marketCondition: this.getMarketCondition(
|
||||
performancePercentFromAllTimeHigh
|
||||
),
|
||||
@ -160,9 +161,13 @@ export class BenchmarkService {
|
||||
performances: {
|
||||
allTimeHigh: {
|
||||
date: allTimeHigh?.date,
|
||||
performancePercent: performancePercentFromAllTimeHigh
|
||||
performancePercent:
|
||||
performancePercentFromAllTimeHigh >= 0
|
||||
? 0
|
||||
: performancePercentFromAllTimeHigh
|
||||
}
|
||||
},
|
||||
symbol: benchmarkAssetProfiles[index].symbol,
|
||||
trend50d: benchmarkTrends[index].trend50d,
|
||||
trend200d: benchmarkTrends[index].trend200d
|
||||
};
|
||||
@ -213,7 +218,7 @@ export class BenchmarkService {
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
public async getMarketDataBySymbol({
|
||||
public async getMarketDataForUser({
|
||||
dataSource,
|
||||
endDate = new Date(),
|
||||
startDate,
|
||||
@ -417,7 +422,7 @@ export class BenchmarkService {
|
||||
private getMarketCondition(
|
||||
aPerformanceInPercent: number
|
||||
): Benchmark['marketCondition'] {
|
||||
if (aPerformanceInPercent === 0) {
|
||||
if (aPerformanceInPercent >= 0) {
|
||||
return 'ALL_TIME_HIGH';
|
||||
} else if (aPerformanceInPercent <= -0.2) {
|
||||
return 'BEAR_MARKET';
|
||||
|
@ -13,10 +13,7 @@ import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/da
|
||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
||||
import {
|
||||
DATA_GATHERING_QUEUE_PRIORITY_HIGH,
|
||||
DATA_GATHERING_QUEUE_PRIORITY_MEDIUM
|
||||
} from '@ghostfolio/common/config';
|
||||
import { DATA_GATHERING_QUEUE_PRIORITY_HIGH } from '@ghostfolio/common/config';
|
||||
import {
|
||||
DATE_FORMAT,
|
||||
getAssetProfileIdentifier,
|
||||
@ -295,6 +292,7 @@ export class ImportService {
|
||||
figi,
|
||||
figiComposite,
|
||||
figiShareClass,
|
||||
holdings,
|
||||
id,
|
||||
isin,
|
||||
name,
|
||||
@ -367,6 +365,7 @@ export class ImportService {
|
||||
figi,
|
||||
figiComposite,
|
||||
figiShareClass,
|
||||
holdings,
|
||||
id,
|
||||
isin,
|
||||
name,
|
||||
@ -538,6 +537,7 @@ export class ImportService {
|
||||
assetSubClass: undefined,
|
||||
countries: undefined,
|
||||
createdAt: undefined,
|
||||
holdings: undefined,
|
||||
id: undefined,
|
||||
sectors: undefined,
|
||||
updatedAt: undefined
|
||||
|
@ -7,7 +7,6 @@ import { ConfigurationModule } from '@ghostfolio/api/services/configuration/conf
|
||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
|
||||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
||||
import { TagModule } from '@ghostfolio/api/services/tag/tag.module';
|
||||
|
@ -1,3 +1,6 @@
|
||||
import { IsCurrencyCode } from '@ghostfolio/api/validators/is-currency-code';
|
||||
import { IsAfter1970Constraint } from '@ghostfolio/common/validator-constraints/is-after-1970';
|
||||
|
||||
import {
|
||||
AssetClass,
|
||||
AssetSubClass,
|
||||
@ -10,12 +13,12 @@ import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsISO4217CurrencyCode,
|
||||
IsISO8601,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Min
|
||||
Min,
|
||||
Validate
|
||||
} from 'class-validator';
|
||||
import { isString } from 'lodash';
|
||||
|
||||
@ -39,10 +42,10 @@ export class CreateOrderDto {
|
||||
)
|
||||
comment?: string;
|
||||
|
||||
@IsISO4217CurrencyCode()
|
||||
@IsCurrencyCode()
|
||||
currency: string;
|
||||
|
||||
@IsISO4217CurrencyCode()
|
||||
@IsCurrencyCode()
|
||||
@IsOptional()
|
||||
customCurrency?: string;
|
||||
|
||||
@ -51,6 +54,7 @@ export class CreateOrderDto {
|
||||
dataSource?: DataSource;
|
||||
|
||||
@IsISO8601()
|
||||
@Validate(IsAfter1970Constraint)
|
||||
date: string;
|
||||
|
||||
@IsNumber()
|
||||
|
@ -66,7 +66,6 @@ export class OrderController {
|
||||
|
||||
return this.orderService.deleteOrders({
|
||||
filters,
|
||||
userCurrency: this.request.user.Settings.settings.baseCurrency,
|
||||
userId: this.request.user.id
|
||||
});
|
||||
}
|
||||
|
@ -10,7 +10,11 @@ import {
|
||||
GATHER_ASSET_PROFILE_PROCESS_OPTIONS
|
||||
} from '@ghostfolio/common/config';
|
||||
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
|
||||
import { Filter, UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||
import {
|
||||
EnhancedSymbolProfile,
|
||||
Filter,
|
||||
UniqueAsset
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
@ -180,7 +184,15 @@ export class OrderService {
|
||||
where
|
||||
});
|
||||
|
||||
if (['FEE', 'INTEREST', 'ITEM', 'LIABILITY'].includes(order.type)) {
|
||||
const [symbolProfile] =
|
||||
await this.symbolProfileService.getSymbolProfilesByIds([
|
||||
order.symbolProfileId
|
||||
]);
|
||||
|
||||
if (
|
||||
['FEE', 'INTEREST', 'ITEM', 'LIABILITY'].includes(order.type) ||
|
||||
symbolProfile.activitiesCount === 0
|
||||
) {
|
||||
await this.symbolProfileService.deleteById(order.symbolProfileId);
|
||||
}
|
||||
|
||||
@ -196,18 +208,16 @@ export class OrderService {
|
||||
|
||||
public async deleteOrders({
|
||||
filters,
|
||||
userCurrency,
|
||||
userId
|
||||
}: {
|
||||
filters?: Filter[];
|
||||
userCurrency: string;
|
||||
userId: string;
|
||||
}): Promise<number> {
|
||||
const { activities } = await this.getOrders({
|
||||
filters,
|
||||
userId,
|
||||
userCurrency,
|
||||
includeDrafts: true,
|
||||
userCurrency: undefined,
|
||||
withExcludedAccounts: true
|
||||
});
|
||||
|
||||
@ -221,6 +231,19 @@ export class OrderService {
|
||||
}
|
||||
});
|
||||
|
||||
const symbolProfiles =
|
||||
await this.symbolProfileService.getSymbolProfilesByIds(
|
||||
activities.map(({ symbolProfileId }) => {
|
||||
return symbolProfileId;
|
||||
})
|
||||
);
|
||||
|
||||
for (const { activitiesCount, id } of symbolProfiles) {
|
||||
if (activitiesCount === 0) {
|
||||
await this.symbolProfileService.deleteById(id);
|
||||
}
|
||||
}
|
||||
|
||||
this.eventEmitter.emit(
|
||||
PortfolioChangedEvent.getName(),
|
||||
new PortfolioChangedEvent({ userId })
|
||||
@ -268,7 +291,8 @@ export class OrderService {
|
||||
withExcludedAccounts?: boolean;
|
||||
}): Promise<Activities> {
|
||||
let orderBy: Prisma.Enumerable<Prisma.OrderOrderByWithRelationInput> = [
|
||||
{ date: 'asc' }
|
||||
{ date: 'asc' },
|
||||
{ id: 'asc' }
|
||||
];
|
||||
const where: Prisma.OrderWhereInput = { userId };
|
||||
|
||||
@ -344,7 +368,7 @@ export class OrderService {
|
||||
}
|
||||
|
||||
if (sortColumn) {
|
||||
orderBy = [{ [sortColumn]: sortDirection }];
|
||||
orderBy = [{ [sortColumn]: sortDirection }, { id: sortDirection }];
|
||||
}
|
||||
|
||||
if (types) {
|
||||
@ -429,6 +453,26 @@ export class OrderService {
|
||||
return { activities, count };
|
||||
}
|
||||
|
||||
public async getStatisticsByCurrency(
|
||||
currency: EnhancedSymbolProfile['currency']
|
||||
): Promise<{
|
||||
activitiesCount: EnhancedSymbolProfile['activitiesCount'];
|
||||
dateOfFirstActivity: EnhancedSymbolProfile['dateOfFirstActivity'];
|
||||
}> {
|
||||
const { _count, _min } = await this.prismaService.order.aggregate({
|
||||
_count: true,
|
||||
_min: {
|
||||
date: true
|
||||
},
|
||||
where: { SymbolProfile: { currency } }
|
||||
});
|
||||
|
||||
return {
|
||||
activitiesCount: _count as number,
|
||||
dateOfFirstActivity: _min.date
|
||||
};
|
||||
}
|
||||
|
||||
public async order(
|
||||
orderWhereUniqueInput: Prisma.OrderWhereUniqueInput
|
||||
): Promise<Order | null> {
|
||||
|
@ -1,3 +1,6 @@
|
||||
import { IsCurrencyCode } from '@ghostfolio/api/validators/is-currency-code';
|
||||
import { IsAfter1970Constraint } from '@ghostfolio/common/validator-constraints/is-after-1970';
|
||||
|
||||
import {
|
||||
AssetClass,
|
||||
AssetSubClass,
|
||||
@ -9,12 +12,12 @@ import { Transform, TransformFnParams } from 'class-transformer';
|
||||
import {
|
||||
IsArray,
|
||||
IsEnum,
|
||||
IsISO4217CurrencyCode,
|
||||
IsISO8601,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Min
|
||||
Min,
|
||||
Validate
|
||||
} from 'class-validator';
|
||||
import { isString } from 'lodash';
|
||||
|
||||
@ -38,10 +41,10 @@ export class UpdateOrderDto {
|
||||
)
|
||||
comment?: string;
|
||||
|
||||
@IsISO4217CurrencyCode()
|
||||
@IsCurrencyCode()
|
||||
currency: string;
|
||||
|
||||
@IsISO4217CurrencyCode()
|
||||
@IsCurrencyCode()
|
||||
@IsOptional()
|
||||
customCurrency?: string;
|
||||
|
||||
@ -49,6 +52,7 @@ export class UpdateOrderDto {
|
||||
dataSource: DataSource;
|
||||
|
||||
@IsISO8601()
|
||||
@Validate(IsAfter1970Constraint)
|
||||
date: string;
|
||||
|
||||
@IsNumber()
|
||||
|
@ -20,6 +20,7 @@ export const symbolProfileDummyData = {
|
||||
assetSubClass: undefined,
|
||||
countries: [],
|
||||
createdAt: undefined,
|
||||
holdings: [],
|
||||
id: undefined,
|
||||
sectors: [],
|
||||
updatedAt: undefined
|
||||
|
@ -204,6 +204,7 @@ export class PortfolioController {
|
||||
: undefined,
|
||||
countries: hasDetails ? portfolioPosition.countries : [],
|
||||
currency: hasDetails ? portfolioPosition.currency : undefined,
|
||||
holdings: hasDetails ? portfolioPosition.holdings : [],
|
||||
markets: hasDetails ? portfolioPosition.markets : undefined,
|
||||
marketsAdvanced: hasDetails
|
||||
? portfolioPosition.marketsAdvanced
|
||||
|
@ -499,6 +499,17 @@ export class PortfolioService {
|
||||
grossPerformancePercentageWithCurrencyEffect?.toNumber() ?? 0,
|
||||
grossPerformanceWithCurrencyEffect:
|
||||
grossPerformanceWithCurrencyEffect?.toNumber() ?? 0,
|
||||
holdings: assetProfile.holdings.map(
|
||||
({ allocationInPercentage, name }) => {
|
||||
return {
|
||||
allocationInPercentage,
|
||||
name,
|
||||
valueInBaseCurrency: valueInBaseCurrency
|
||||
.mul(allocationInPercentage)
|
||||
.toNumber()
|
||||
};
|
||||
}
|
||||
),
|
||||
investment: investment.toNumber(),
|
||||
marketState: dataProviderResponse?.marketState ?? 'delayed',
|
||||
name: assetProfile.name,
|
||||
@ -1465,6 +1476,7 @@ export class PortfolioService {
|
||||
grossPerformancePercent: 0,
|
||||
grossPerformancePercentWithCurrencyEffect: 0,
|
||||
grossPerformanceWithCurrencyEffect: 0,
|
||||
holdings: [],
|
||||
investment: balance,
|
||||
marketPrice: 0,
|
||||
marketState: 'open',
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||
import {
|
||||
DATE_FORMAT,
|
||||
getYesterday,
|
||||
interpolate
|
||||
} from '@ghostfolio/common/helper';
|
||||
import { personalFinanceTools } from '@ghostfolio/common/personal-finance-tools';
|
||||
|
||||
import { Controller, Get, Res, VERSION_NEUTRAL, Version } from '@nestjs/common';
|
||||
import { format } from 'date-fns';
|
||||
@ -14,7 +16,9 @@ import * as path from 'path';
|
||||
export class SitemapController {
|
||||
public sitemapXml = '';
|
||||
|
||||
public constructor() {
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService
|
||||
) {
|
||||
try {
|
||||
this.sitemapXml = fs.readFileSync(
|
||||
path.join(__dirname, 'assets', 'sitemap.xml'),
|
||||
@ -25,11 +29,51 @@ export class SitemapController {
|
||||
|
||||
@Get()
|
||||
@Version(VERSION_NEUTRAL)
|
||||
public async flushCache(@Res() response: Response): Promise<void> {
|
||||
public async getSitemapXml(@Res() response: Response): Promise<void> {
|
||||
const currentDate = format(getYesterday(), DATE_FORMAT);
|
||||
|
||||
response.setHeader('content-type', 'application/xml');
|
||||
response.send(
|
||||
interpolate(this.sitemapXml, {
|
||||
currentDate: format(getYesterday(), DATE_FORMAT)
|
||||
currentDate,
|
||||
personalFinanceTools: this.configurationService.get(
|
||||
'ENABLE_FEATURE_SUBSCRIPTION'
|
||||
)
|
||||
? personalFinanceTools
|
||||
.map(({ alias, key }) => {
|
||||
return [
|
||||
'<url>',
|
||||
` <loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-${alias ?? key}</loc>`,
|
||||
` <lastmod>${currentDate}T00:00:00+00:00</lastmod>`,
|
||||
'</url>',
|
||||
'<url>',
|
||||
` <loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-${alias ?? key}</loc>`,
|
||||
` <lastmod>${currentDate}T00:00:00+00:00</lastmod>`,
|
||||
'</url>',
|
||||
'<url>',
|
||||
` <loc>https://ghostfol.io/es/recursos/personal-finance-tools/alternativa-de-software-libre-a-${alias ?? key}</loc>`,
|
||||
` <lastmod>${currentDate}T00:00:00+00:00</lastmod>`,
|
||||
'</url>',
|
||||
'<url>',
|
||||
` <loc>https://ghostfol.io/fr/ressources/personal-finance-tools/alternative-open-source-a-${alias ?? key}</loc>`,
|
||||
` <lastmod>${currentDate}T00:00:00+00:00</lastmod>`,
|
||||
'</url>',
|
||||
'<url>',
|
||||
` <loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-${alias ?? key}</loc>`,
|
||||
` <lastmod>${currentDate}T00:00:00+00:00</lastmod>`,
|
||||
'</url>',
|
||||
'<url>',
|
||||
` <loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-${alias ?? key}</loc>`,
|
||||
` <lastmod>${currentDate}T00:00:00+00:00</lastmod>`,
|
||||
'</url>',
|
||||
'<url>',
|
||||
` <loc>https://ghostfol.io/pt/recursos/personal-finance-tools/alternativa-de-software-livre-ao-${alias ?? key}</loc>`,
|
||||
` <lastmod>${currentDate}T00:00:00+00:00</lastmod>`,
|
||||
'</url>'
|
||||
].join('\n');
|
||||
})
|
||||
.join('\n')
|
||||
: ''
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -1,8 +1,11 @@
|
||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { SitemapController } from './sitemap.controller';
|
||||
|
||||
@Module({
|
||||
controllers: [SitemapController]
|
||||
controllers: [SitemapController],
|
||||
imports: [ConfigurationModule]
|
||||
})
|
||||
export class SitemapModule {}
|
||||
|
@ -22,7 +22,7 @@ export class SubscriptionService {
|
||||
this.stripe = new Stripe(
|
||||
this.configurationService.get('STRIPE_SECRET_KEY'),
|
||||
{
|
||||
apiVersion: '2022-11-15'
|
||||
apiVersion: '2024-04-10'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { IsCurrencyCode } from '@ghostfolio/api/validators/is-currency-code';
|
||||
import type {
|
||||
ColorScheme,
|
||||
DateRange,
|
||||
@ -7,7 +8,6 @@ import type {
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsISO4217CurrencyCode,
|
||||
IsISO8601,
|
||||
IsIn,
|
||||
IsNumber,
|
||||
@ -21,7 +21,7 @@ export class UpdateUserSettingDto {
|
||||
@IsOptional()
|
||||
annualInterestRate?: number;
|
||||
|
||||
@IsISO4217CurrencyCode()
|
||||
@IsCurrencyCode()
|
||||
@IsOptional()
|
||||
baseCurrency?: string;
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
|
||||
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
|
||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||
@ -19,6 +20,7 @@ import { UserService } from './user.service';
|
||||
secret: process.env.JWT_SECRET_KEY,
|
||||
signOptions: { expiresIn: '30 days' }
|
||||
}),
|
||||
OrderModule,
|
||||
PrismaModule,
|
||||
PropertyModule,
|
||||
SubscriptionModule,
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
||||
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
|
||||
import { environment } from '@ghostfolio/api/environments/environment';
|
||||
import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event';
|
||||
@ -40,6 +41,7 @@ export class UserService {
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly orderService: OrderService,
|
||||
private readonly prismaService: PrismaService,
|
||||
private readonly propertyService: PropertyService,
|
||||
private readonly subscriptionService: SubscriptionService,
|
||||
@ -398,8 +400,8 @@ export class UserService {
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
await this.prismaService.order.deleteMany({
|
||||
where: { userId: where.id }
|
||||
await this.orderService.deleteOrders({
|
||||
userId: where.id
|
||||
});
|
||||
} catch {}
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -54,230 +54,6 @@
|
||||
<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-allinvestview</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-allvue-systems</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-basil-finance</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>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-compound-planning</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-copilot-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-de.fi</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-delta</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-divvydiary</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-empower</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-exirio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-fina</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<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>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-getquin</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<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>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-koyfin</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-kubera</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-magnifi</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-markets.sh</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-maybe-finance</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-monarch-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-monse</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-navexa</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-parqet</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-plannix</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-portfolio-dividend-tracker</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-portfolio-visualizer</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-portseido</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<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>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-sharesight</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-simple-portfolio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-snowball-analytics</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-stock-events</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-stockle</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-stockmarketeye</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-stonksfolio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-sumio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-tiller</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<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-wallmine</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-wealthfolio</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-whal</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>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-ynab</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ueber-uns</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -432,230 +208,6 @@
|
||||
<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-allinvestview</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-allvue-systems</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-basil-finance</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>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-compound-planning</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-copilot-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-de.fi</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-delta</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-divvydiary</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-empower</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-exirio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-fina</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<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>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-getquin</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<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>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-koyfin</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-kubera</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-magnifi</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-markets.sh</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-maybe-finance</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-monarch-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-monse</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-navexa</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-parqet</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-plannix</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-portfolio-dividend-tracker</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-portfolio-visualizer</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-portseido</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<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>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-sharesight</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-simple-portfolio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-snowball-analytics</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-stock-events</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-stockle</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-stockmarketeye</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-stonksfolio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-sumio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-tiller</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<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-wallmine</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-wealthfolio</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-whal</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>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-ynab</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/es</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -686,6 +238,10 @@
|
||||
<loc>https://ghostfol.io/es/recursos</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/es/recursos/personal-finance-tools</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/es/registro</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -764,6 +320,10 @@
|
||||
<loc>https://ghostfol.io/fr/ressources</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/fr/ressources/personal-finance-tools</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -822,230 +382,6 @@
|
||||
<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-allinvestview</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-allvue-systems</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-basil-finance</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<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>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-compound-planning</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-copilot-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-de.fi</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-delta</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-divvydiary</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-empower</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-exirio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-fina</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<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>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-getquin</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<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>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-koyfin</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-kubera</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-magnifi</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-markets.sh</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-maybe-finance</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-monarch-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-monse</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-navexa</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-parqet</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-plannix</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-portfolio-dividend-tracker</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-portfolio-visualizer</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-portseido</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<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>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-sharesight</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-simple-portfolio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-snowball-analytics</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-stock-events</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-stockle</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-stockmarketeye</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-stonksfolio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-sumio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-tiller</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<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-wallmine</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-wealthfolio</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-whal</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>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-ynab</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -1058,230 +394,6 @@
|
||||
<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-allinvestview</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-allvue-systems</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-basil-finance</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>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-compound-planning</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-copilot-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-de.fi</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-delta</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-divvydiary</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-empower</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-exirio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-fina</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<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>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-getquin</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<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>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-koyfin</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-kubera</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-magnifi</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-markets.sh</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-maybe-finance</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-monarch-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-monse</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-navexa</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-parqet</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-plannix</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-portfolio-dividend-tracker</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-portfolio-visualizer</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-portseido</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<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>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-sharesight</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-simple-portfolio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-snowball-analytics</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-stock-events</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-stockle</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-stockmarketeye</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-stonksfolio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-sumio</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-tiller</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<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-wallmine</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-wealthfolio</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-whal</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>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-ynab</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/functionaliteiten</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -1366,6 +478,10 @@
|
||||
<loc>https://ghostfol.io/pt/recursos</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/pt/recursos/personal-finance-tools</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/pt/registo</loc>
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
@ -1400,4 +516,5 @@
|
||||
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
-->
|
||||
${personalFinanceTools}
|
||||
</urlset>
|
||||
|
@ -55,10 +55,10 @@ export class AccountClusterRiskCurrentInvestment extends Rule<Settings> {
|
||||
|
||||
const maxInvestmentRatio = maxItem?.investment / totalInvestment || 0;
|
||||
|
||||
if (maxInvestmentRatio > ruleSettings.threshold) {
|
||||
if (maxInvestmentRatio > ruleSettings.thresholdMax) {
|
||||
return {
|
||||
evaluation: `Over ${
|
||||
ruleSettings.threshold * 100
|
||||
ruleSettings.thresholdMax * 100
|
||||
}% of your current investment is at ${maxItem.name} (${(
|
||||
maxInvestmentRatio * 100
|
||||
).toPrecision(3)}%)`,
|
||||
@ -70,7 +70,7 @@ export class AccountClusterRiskCurrentInvestment extends Rule<Settings> {
|
||||
evaluation: `The major part of your current investment is at ${
|
||||
maxItem.name
|
||||
} (${(maxInvestmentRatio * 100).toPrecision(3)}%) and does not exceed ${
|
||||
ruleSettings.threshold * 100
|
||||
ruleSettings.thresholdMax * 100
|
||||
}%`,
|
||||
value: true
|
||||
};
|
||||
@ -80,12 +80,12 @@ export class AccountClusterRiskCurrentInvestment extends Rule<Settings> {
|
||||
return {
|
||||
baseCurrency: aUserSettings.baseCurrency,
|
||||
isActive: true,
|
||||
threshold: 0.5
|
||||
thresholdMax: 0.5
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface Settings extends RuleSettings {
|
||||
baseCurrency: string;
|
||||
threshold: number;
|
||||
thresholdMax: number;
|
||||
}
|
||||
|
@ -41,10 +41,10 @@ export class CurrencyClusterRiskCurrentInvestment extends Rule<Settings> {
|
||||
|
||||
const maxValueRatio = maxItem?.value / totalValue || 0;
|
||||
|
||||
if (maxValueRatio > ruleSettings.threshold) {
|
||||
if (maxValueRatio > ruleSettings.thresholdMax) {
|
||||
return {
|
||||
evaluation: `Over ${
|
||||
ruleSettings.threshold * 100
|
||||
ruleSettings.thresholdMax * 100
|
||||
}% of your current investment is in ${maxItem.groupKey} (${(
|
||||
maxValueRatio * 100
|
||||
).toPrecision(3)}%)`,
|
||||
@ -56,7 +56,7 @@ export class CurrencyClusterRiskCurrentInvestment extends Rule<Settings> {
|
||||
evaluation: `The major part of your current investment is in ${
|
||||
maxItem?.groupKey ?? ruleSettings.baseCurrency
|
||||
} (${(maxValueRatio * 100).toPrecision(3)}%) and does not exceed ${
|
||||
ruleSettings.threshold * 100
|
||||
ruleSettings.thresholdMax * 100
|
||||
}%`,
|
||||
value: true
|
||||
};
|
||||
@ -66,12 +66,12 @@ export class CurrencyClusterRiskCurrentInvestment extends Rule<Settings> {
|
||||
return {
|
||||
baseCurrency: aUserSettings.baseCurrency,
|
||||
isActive: true,
|
||||
threshold: 0.5
|
||||
thresholdMax: 0.5
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface Settings extends RuleSettings {
|
||||
baseCurrency: string;
|
||||
threshold: number;
|
||||
thresholdMax: number;
|
||||
}
|
||||
|
@ -19,16 +19,16 @@ export class EmergencyFundSetup extends Rule<Settings> {
|
||||
}
|
||||
|
||||
public evaluate(ruleSettings: Settings) {
|
||||
if (this.emergencyFund > ruleSettings.threshold) {
|
||||
if (this.emergencyFund < ruleSettings.thresholdMin) {
|
||||
return {
|
||||
evaluation: 'An emergency fund has been set up',
|
||||
value: true
|
||||
evaluation: 'No emergency fund has been set up',
|
||||
value: false
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
evaluation: 'No emergency fund has been set up',
|
||||
value: false
|
||||
evaluation: 'An emergency fund has been set up',
|
||||
value: true
|
||||
};
|
||||
}
|
||||
|
||||
@ -36,12 +36,12 @@ export class EmergencyFundSetup extends Rule<Settings> {
|
||||
return {
|
||||
baseCurrency: aUserSettings.baseCurrency,
|
||||
isActive: true,
|
||||
threshold: 0
|
||||
thresholdMin: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface Settings extends RuleSettings {
|
||||
baseCurrency: string;
|
||||
threshold: number;
|
||||
thresholdMin: number;
|
||||
}
|
||||
|
@ -26,10 +26,10 @@ export class FeeRatioInitialInvestment extends Rule<Settings> {
|
||||
? this.fees / this.totalInvestment
|
||||
: 0;
|
||||
|
||||
if (feeRatio > ruleSettings.threshold) {
|
||||
if (feeRatio > ruleSettings.thresholdMax) {
|
||||
return {
|
||||
evaluation: `The fees do exceed ${
|
||||
ruleSettings.threshold * 100
|
||||
ruleSettings.thresholdMax * 100
|
||||
}% of your initial investment (${(feeRatio * 100).toPrecision(3)}%)`,
|
||||
value: false
|
||||
};
|
||||
@ -37,7 +37,7 @@ export class FeeRatioInitialInvestment extends Rule<Settings> {
|
||||
|
||||
return {
|
||||
evaluation: `The fees do not exceed ${
|
||||
ruleSettings.threshold * 100
|
||||
ruleSettings.thresholdMax * 100
|
||||
}% of your initial investment (${(feeRatio * 100).toPrecision(3)}%)`,
|
||||
value: true
|
||||
};
|
||||
@ -47,12 +47,12 @@ export class FeeRatioInitialInvestment extends Rule<Settings> {
|
||||
return {
|
||||
baseCurrency: aUserSettings.baseCurrency,
|
||||
isActive: true,
|
||||
threshold: 0.01
|
||||
thresholdMax: 0.01
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface Settings extends RuleSettings {
|
||||
baseCurrency: string;
|
||||
threshold: number;
|
||||
thresholdMax: number;
|
||||
}
|
||||
|
@ -181,6 +181,7 @@ export class DataGatheringService {
|
||||
figi,
|
||||
figiComposite,
|
||||
figiShareClass,
|
||||
holdings,
|
||||
isin,
|
||||
name,
|
||||
sectors,
|
||||
@ -198,6 +199,7 @@ export class DataGatheringService {
|
||||
figi,
|
||||
figiComposite,
|
||||
figiShareClass,
|
||||
holdings,
|
||||
isin,
|
||||
name,
|
||||
sectors,
|
||||
@ -212,6 +214,7 @@ export class DataGatheringService {
|
||||
figi,
|
||||
figiComposite,
|
||||
figiShareClass,
|
||||
holdings,
|
||||
isin,
|
||||
name,
|
||||
sectors,
|
||||
|
@ -36,6 +36,7 @@ export class DataEnhancerService {
|
||||
|
||||
if (
|
||||
(assetProfile.countries as unknown as Prisma.JsonArray)?.length > 0 &&
|
||||
(assetProfile.holdings as unknown as Prisma.JsonArray)?.length > 0 &&
|
||||
(assetProfile.sectors as unknown as Prisma.JsonArray)?.length > 0
|
||||
) {
|
||||
return true;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
|
||||
import { Holding } from '@ghostfolio/common/interfaces';
|
||||
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
||||
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
||||
|
||||
@ -155,11 +156,30 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!response.holdings ||
|
||||
(response.holdings as unknown as Holding[]).length === 0
|
||||
) {
|
||||
response.holdings = [];
|
||||
|
||||
for (const { label, weight } of holdings?.topHoldings ?? []) {
|
||||
if (label?.toLowerCase() === 'other') {
|
||||
continue;
|
||||
}
|
||||
|
||||
response.holdings.push({
|
||||
weight,
|
||||
name: label
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!response.sectors ||
|
||||
(response.sectors as unknown as Sector[]).length === 0
|
||||
) {
|
||||
response.sectors = [];
|
||||
|
||||
for (const [name, value] of Object.entries<any>(
|
||||
holdings?.sectors ?? {}
|
||||
)) {
|
||||
|
@ -516,7 +516,8 @@ export class DataProviderService {
|
||||
.filter((symbol) => {
|
||||
return (
|
||||
isNumber(response[symbol].marketPrice) &&
|
||||
response[symbol].marketPrice > 0
|
||||
response[symbol].marketPrice > 0 &&
|
||||
response[symbol].marketState === 'open'
|
||||
);
|
||||
})
|
||||
.map((symbol) => {
|
||||
|
@ -246,7 +246,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
for (const { close, code, timestamp } of quotes) {
|
||||
let currency: string;
|
||||
|
||||
if (code.endsWith('.FOREX')) {
|
||||
if (this.isForex(code)) {
|
||||
currency = this.convertFromEodSymbol(code)?.replace(
|
||||
DEFAULT_CURRENCY,
|
||||
''
|
||||
@ -272,7 +272,10 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
currency,
|
||||
dataSource: this.getName(),
|
||||
marketPrice: close,
|
||||
marketState: isToday(new Date(timestamp * 1000)) ? 'open' : 'closed'
|
||||
marketState:
|
||||
this.isForex(code) || isToday(new Date(timestamp * 1000))
|
||||
? 'open'
|
||||
: 'closed'
|
||||
};
|
||||
} else {
|
||||
Logger.error(
|
||||
@ -311,7 +314,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
items: searchResult
|
||||
.filter(({ currency, symbol }) => {
|
||||
// Remove 'NA' currency and exchange rates
|
||||
return currency?.length === 3 && !symbol.endsWith('.FOREX');
|
||||
return currency?.length === 3 && !this.isForex(symbol);
|
||||
})
|
||||
.map(
|
||||
({
|
||||
@ -349,7 +352,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
private convertFromEodSymbol(aEodSymbol: string) {
|
||||
let symbol = aEodSymbol;
|
||||
|
||||
if (symbol.endsWith('.FOREX')) {
|
||||
if (this.isForex(symbol)) {
|
||||
symbol = symbol.replace('GBX', 'GBp');
|
||||
symbol = symbol.replace('.FOREX', '');
|
||||
}
|
||||
@ -451,6 +454,10 @@ export class EodHistoricalDataService implements DataProviderInterface {
|
||||
return searchResult;
|
||||
}
|
||||
|
||||
private isForex(aCode: string) {
|
||||
return aCode?.endsWith('.FOREX') || false;
|
||||
}
|
||||
|
||||
private parseAssetClass({
|
||||
Exchange,
|
||||
Type
|
||||
|
@ -167,9 +167,10 @@ export class ManualService implements DataProviderInterface {
|
||||
});
|
||||
|
||||
for (const { currency, symbol } of symbolProfiles) {
|
||||
let marketPrice = marketData.find((marketDataItem) => {
|
||||
return marketDataItem.symbol === symbol;
|
||||
})?.marketPrice;
|
||||
let marketPrice =
|
||||
marketData.find((marketDataItem) => {
|
||||
return marketDataItem.symbol === symbol;
|
||||
})?.marketPrice ?? 0;
|
||||
|
||||
response[symbol] = {
|
||||
currency,
|
||||
@ -256,7 +257,7 @@ export class ManualService implements DataProviderInterface {
|
||||
signal: abortController.signal
|
||||
});
|
||||
|
||||
if (headers['content-type'] === 'application/json') {
|
||||
if (headers['content-type'].includes('application/json')) {
|
||||
const data = JSON.parse(body);
|
||||
const value = String(
|
||||
jsonpath.query(data, scraperConfiguration.selector)[0]
|
||||
|
@ -2,6 +2,7 @@ import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||
import {
|
||||
EnhancedSymbolProfile,
|
||||
Holding,
|
||||
ScraperConfiguration,
|
||||
UniqueAsset
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
@ -97,6 +98,7 @@ export class SymbolProfileService {
|
||||
countries,
|
||||
currency,
|
||||
dataSource,
|
||||
holdings,
|
||||
name,
|
||||
scraperConfiguration,
|
||||
sectors,
|
||||
@ -112,6 +114,7 @@ export class SymbolProfileService {
|
||||
comment,
|
||||
countries,
|
||||
currency,
|
||||
holdings,
|
||||
name,
|
||||
scraperConfiguration,
|
||||
sectors,
|
||||
@ -140,6 +143,7 @@ export class SymbolProfileService {
|
||||
symbolProfile?.countries as unknown as Prisma.JsonArray
|
||||
),
|
||||
dateOfFirstActivity: <Date>undefined,
|
||||
holdings: this.getHoldings(symbolProfile),
|
||||
scraperConfiguration: this.getScraperConfiguration(symbolProfile),
|
||||
sectors: this.getSectors(symbolProfile),
|
||||
symbolMapping: this.getSymbolMapping(symbolProfile)
|
||||
@ -167,6 +171,14 @@ export class SymbolProfileService {
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
(item.SymbolProfileOverrides.holdings as unknown as Holding[])
|
||||
?.length > 0
|
||||
) {
|
||||
item.holdings = item.SymbolProfileOverrides
|
||||
.holdings as unknown as Holding[];
|
||||
}
|
||||
|
||||
item.name = item.SymbolProfileOverrides?.name ?? item.name;
|
||||
|
||||
if (
|
||||
@ -203,6 +215,20 @@ export class SymbolProfileService {
|
||||
});
|
||||
}
|
||||
|
||||
private getHoldings(symbolProfile: SymbolProfile): Holding[] {
|
||||
return ((symbolProfile?.holdings as Prisma.JsonArray) ?? []).map(
|
||||
(holding) => {
|
||||
const { name, weight } = holding as Prisma.JsonObject;
|
||||
|
||||
return {
|
||||
allocationInPercentage: weight as number,
|
||||
name: (name as string) ?? UNKNOWN_KEY,
|
||||
valueInBaseCurrency: undefined
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private getScraperConfiguration(
|
||||
symbolProfile: SymbolProfile
|
||||
): ScraperConfiguration {
|
||||
|
@ -70,7 +70,7 @@ export class TwitterBotService {
|
||||
await this.twitterClient.v2.tweet(status);
|
||||
|
||||
Logger.log(
|
||||
`Fear & Greed Index has been tweeted: https://twitter.com/ghostfolio_/status/${createdTweet.id}`,
|
||||
`Fear & Greed Index has been posted: https://x.com/ghostfolio_/status/${createdTweet.id}`,
|
||||
'TwitterBotService'
|
||||
);
|
||||
}
|
||||
|
44
apps/api/src/validators/is-currency-code.ts
Normal file
44
apps/api/src/validators/is-currency-code.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { DERIVED_CURRENCIES } from '@ghostfolio/common/config';
|
||||
|
||||
import {
|
||||
registerDecorator,
|
||||
ValidationOptions,
|
||||
ValidatorConstraint,
|
||||
ValidatorConstraintInterface,
|
||||
ValidationArguments
|
||||
} from 'class-validator';
|
||||
import { isISO4217CurrencyCode } from 'class-validator';
|
||||
|
||||
export function IsCurrencyCode(validationOptions?: ValidationOptions) {
|
||||
return function (object: Object, propertyName: string) {
|
||||
registerDecorator({
|
||||
propertyName,
|
||||
constraints: [],
|
||||
options: validationOptions,
|
||||
target: object.constructor,
|
||||
validator: IsExtendedCurrencyConstraint
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@ValidatorConstraint({ async: false })
|
||||
export class IsExtendedCurrencyConstraint
|
||||
implements ValidatorConstraintInterface
|
||||
{
|
||||
public defaultMessage(args: ValidationArguments) {
|
||||
return '$value must be a valid ISO4217 currency code';
|
||||
}
|
||||
|
||||
public validate(currency: any) {
|
||||
// Return true if currency is a standard ISO 4217 code or a derived currency
|
||||
return (
|
||||
isISO4217CurrencyCode(currency) ||
|
||||
[
|
||||
...DERIVED_CURRENCIES.map((derivedCurrency) => {
|
||||
return derivedCurrency.currency;
|
||||
}),
|
||||
'USX'
|
||||
].includes(currency)
|
||||
);
|
||||
}
|
||||
}
|
18
apps/client/localhost.cert
Normal file
18
apps/client/localhost.cert
Normal file
@ -0,0 +1,18 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIC5TCCAc2gAwIBAgIJAJAMHOFnJ6oyMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV
|
||||
BAMMCWxvY2FsaG9zdDAeFw0yNDAyMjcxNTQ2MzFaFw0yNDAzMjgxNTQ2MzFaMBQx
|
||||
EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
|
||||
ggEBAJ0hRjViikEKVIyukXR4CfuYVvFEFzB6AwAQ9Jrz2MseqpLacLTXFFAS54mp
|
||||
iDuqPBzs9ta40mlSrqSBuAWKikpW5kTNnmqUnDZ6iSJezbYWx9YyULGqqb1q3C4/
|
||||
5pH9m6NHJ+2uaUNKlDxYNKbntqs3drQEdxH9yv672Z53nvogTcf9jz6zjutEQGSV
|
||||
HgVkCTTQmzf3+3st+VJ5D8JeYFR+tpZ6yleqgXFaTMtPZRfKLvTkQ+KeyCJLnsUJ
|
||||
BQvdCKI0PGsG6d6ygXFmSePolD9KW3VTKKDPCsndID89vAnRWDj9UhzvycxmKpcF
|
||||
GrUPH5+Pis1PM1R7OnAvnFygnyECAwEAAaM6MDgwFAYDVR0RBA0wC4IJbG9jYWxo
|
||||
b3N0MAsGA1UdDwQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0B
|
||||
AQsFAAOCAQEAOdzrY3RTAPnBVAd3/GKIEkielYyOgKPnvd+RcOB+je8cXZY+vaxX
|
||||
uEFP0526G00+kRZ2Tx9t0SmjoSpwg3lHUPMko0dIxgRlqISDAohdrEptHpcVujsD
|
||||
ak88LLnAurr60JNjWX2wbEoJ18KLtqGSnATdmCgKwDPIN5a7+ssp44BGyzH6VYCg
|
||||
wV3VjJl0pp4C5M0Jfu0p1FrQjzIVhgqR7JFYmvqIogWrGwYAQK/3XRXq8t48J5X3
|
||||
IsfWiLAA2ZdCoWAnZ6PAGBOoGivtkJm967pHjd/28qYY6mQo4sN2ksEOjx6/YslF
|
||||
2mOJdLV/DzqoggUsTpPEG0dRhzQLTGHKDQ==
|
||||
-----END CERTIFICATE-----
|
28
apps/client/localhost.pem
Normal file
28
apps/client/localhost.pem
Normal file
@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCdIUY1YopBClSM
|
||||
rpF0eAn7mFbxRBcwegMAEPSa89jLHqqS2nC01xRQEueJqYg7qjwc7PbWuNJpUq6k
|
||||
gbgFiopKVuZEzZ5qlJw2eokiXs22FsfWMlCxqqm9atwuP+aR/ZujRyftrmlDSpQ8
|
||||
WDSm57arN3a0BHcR/cr+u9med576IE3H/Y8+s47rREBklR4FZAk00Js39/t7LflS
|
||||
eQ/CXmBUfraWespXqoFxWkzLT2UXyi705EPinsgiS57FCQUL3QiiNDxrBunesoFx
|
||||
Zknj6JQ/Slt1UyigzwrJ3SA/PbwJ0Vg4/VIc78nMZiqXBRq1Dx+fj4rNTzNUezpw
|
||||
L5xcoJ8hAgMBAAECggEAPU5YOEgELTA8oM8TjV+wdWuQsH2ilpVkSkhTR4nQkh+a
|
||||
6cU0qDoqgLt/fySYNL9MyPRjso9V+SX7YdAC3paZMjwJh9q57lehQ1g33SMkG+Fz
|
||||
gs0K0ucFZxQkaB8idN+ANAp1N7UO+ORGRe0cTeqmSNNRCxea5XgiFZVxaPS/IFOR
|
||||
vVdXS1DulgwHh4H0ljKmkj7jc9zPBSc9ccW5Ml2q4a26Atu4IC/Mlp/DF4GNELbD
|
||||
ebi9ZOZG33ip2bdhj3WX7NW9GJaaViKtVUpcpR6u+6BfvTXQ6xrqdoxXk9fnPzzf
|
||||
sSoLPTt8yO4RavP1zQU22PhgIcWudhCiy/2Nv+uLqQKBgQDMPh1/xwdFl8yES8dy
|
||||
f0xJyCDWPiB+xzGwcOAC90FrNZaqG0WWs3VHE4cilaWjCflvdR6aAEDEY68sZJhl
|
||||
h+ChT/g61QLMOI+VIDQ1kJXKYgfS/B+BE1PZ0EkuymKFOvbNO8agHodB8CqnZaQh
|
||||
bLuZaDnZ0JLK4im3KPt70+aKYwKBgQDE8s6xl0SLu+yJLo3VklCRD67Z8/jlvx2t
|
||||
h3DF6NG8pB3QmvKdJWFKuLAWLGflJwbUW9Pt3hXkc0649KQrtiIYC0ZMh9KMaVCk
|
||||
WmjS/n2eIUQZ7ZUlwFesi4p4iGynVBavIY8TJ6Y+K3TvsJgXP3IZ96r689PQJo8E
|
||||
KbSeyYzFqwKBgGQTS4EAlJ+U8bEhMGj51veQCAbyChoUoFRD+n95h6RwbZKMKlzd
|
||||
MenRt7VKfg6VJJNoX8Y1uYaBEaQ+5i1Zlsdz1718AhLu4+u+C9bzMXIo9ox63TTx
|
||||
s3RWioVSxVNiwOtvDrQGQWAdvcioFPQLwyA34aDIgiTHDImimxbhjWThAoGAWOqW
|
||||
Tq9QjxWk0Lpn5ohMP3GpK1VuhasnJvUDARb/uf8ORuPtrOz3Y9jGBvy9W0OnXbCn
|
||||
mbiugZldbTtl8yYjdl+AuYSIlkPl2I3IzZl/9Shnqp0MvSJ9crT9KzXMeC8Knr6z
|
||||
7Z30/BR6ksxTngtS5E5grzPt6Qe/gc2ich3kpEkCgYBfBHUhVIKVrDW/8a7U2679
|
||||
Wj4i9us/M3iPMxcTv7/ZAg08TEvNBQYtvVQLu0NfDKXx8iGKJJ6evIYgNXVm2sPq
|
||||
VzSgwGCALi1X0443amZU+mnX+/9RnBqyM+PJU8570mM83jqKlY/BEsi0aqxTioFG
|
||||
an3xbjjN+Rq9iKLzmPxIMg==
|
||||
-----END PRIVATE KEY-----
|
@ -163,8 +163,11 @@
|
||||
"serve": {
|
||||
"executor": "@nx/angular:dev-server",
|
||||
"options": {
|
||||
"buildTarget": "client:build",
|
||||
"proxyConfig": "apps/client/proxy.conf.json",
|
||||
"buildTarget": "client:build"
|
||||
"ssl": true,
|
||||
"sslCert": "apps/client/localhost.cert",
|
||||
"sslKey": "apps/client/localhost.pem"
|
||||
},
|
||||
"configurations": {
|
||||
"development-de": {
|
||||
|
@ -1,33 +1,31 @@
|
||||
<header>
|
||||
<div
|
||||
*ngIf="canCreateAccount || user?.systemMessage"
|
||||
class="info-message-container"
|
||||
>
|
||||
<div class="info-message-inner-container position-fixed w-100">
|
||||
<div class="align-items-center d-flex h-100 justify-content-center">
|
||||
<a
|
||||
*ngIf="canCreateAccount"
|
||||
class="text-center"
|
||||
[routerLink]="routerLinkRegister"
|
||||
>
|
||||
<div
|
||||
class="cursor-pointer d-inline-block info-message"
|
||||
(click)="onCreateAccount()"
|
||||
>
|
||||
<span i18n>You are using the Live Demo.</span>
|
||||
<span class="a ml-2" i18n>Create Account</span>
|
||||
</div></a
|
||||
>
|
||||
<div
|
||||
*ngIf="!canCreateAccount && user?.systemMessage"
|
||||
class="cursor-pointer d-inline-block info-message text-truncate"
|
||||
(click)="onClickSystemMessage()"
|
||||
>
|
||||
{{ user.systemMessage.message }}
|
||||
@if (canCreateAccount || user?.systemMessage) {
|
||||
<div class="info-message-container">
|
||||
<div class="info-message-inner-container position-fixed w-100">
|
||||
<div class="align-items-center d-flex h-100 justify-content-center">
|
||||
@if (canCreateAccount) {
|
||||
<a class="text-center" [routerLink]="routerLinkRegister">
|
||||
<div
|
||||
class="cursor-pointer d-inline-block info-message"
|
||||
(click)="onCreateAccount()"
|
||||
>
|
||||
<span i18n>You are using the Live Demo.</span>
|
||||
<span class="a ml-2" i18n>Create Account</span>
|
||||
</div></a
|
||||
>
|
||||
}
|
||||
@if (!canCreateAccount && user?.systemMessage) {
|
||||
<div
|
||||
class="cursor-pointer d-inline-block info-message text-truncate"
|
||||
(click)="onClickSystemMessage()"
|
||||
>
|
||||
{{ user.systemMessage.message }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<gf-header
|
||||
class="position-fixed w-100"
|
||||
@ -45,144 +43,159 @@
|
||||
<router-outlet></router-outlet>
|
||||
</main>
|
||||
|
||||
<footer *ngIf="showFooter" class="d-flex justify-content-center py-4 w-100">
|
||||
<div class="container">
|
||||
<div class="mb-3 row">
|
||||
<div class="col-sm">
|
||||
<a [routerLink]="['/']"><gf-logo /></a>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="h6 mt-2" i18n>Personal Finance</div>
|
||||
<ul class="list-unstyled">
|
||||
<li *ngIf="hasPermissionToAccessFearAndGreedIndex">
|
||||
<a i18n [routerLink]="routerLinkMarkets">Markets</a>
|
||||
</li>
|
||||
<li><a i18n [routerLink]="routerLinkResources">Resources</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="h6 mt-2">Ghostfolio</div>
|
||||
<ul class="list-unstyled">
|
||||
<li><a i18n [routerLink]="routerLinkAbout">About</a></li>
|
||||
<li *ngIf="hasPermissionForSubscription">
|
||||
<a i18n [routerLink]="['/blog']">Blog</a>
|
||||
</li>
|
||||
<li>
|
||||
<a i18n [routerLink]="routerLinkAboutChangelog">Changelog</a>
|
||||
</li>
|
||||
<li><a i18n [routerLink]="routerLinkFeatures">Features</a></li>
|
||||
<li *ngIf="hasPermissionForSubscription">
|
||||
<a i18n [routerLink]="routerLinkFaq"
|
||||
>Frequently Asked Questions (FAQ)</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a i18n [routerLink]="routerLinkAboutLicense">License</a>
|
||||
</li>
|
||||
<li *ngIf="hasPermissionForStatistics">
|
||||
<a [routerLink]="['/open']">Open Startup</a>
|
||||
</li>
|
||||
<li *ngIf="hasPermissionForSubscription">
|
||||
<a i18n [routerLink]="routerLinkPricing">Pricing</a>
|
||||
</li>
|
||||
<li *ngIf="hasPermissionForSubscription">
|
||||
<a i18n [routerLink]="routerLinkAboutPrivacyPolicy"
|
||||
>Privacy Policy</a
|
||||
>
|
||||
</li>
|
||||
<li *ngIf="hasPermissionForSubscription">
|
||||
<a
|
||||
class="align-items-baseline d-flex"
|
||||
href="https://status.ghostfol.io"
|
||||
target="_blank"
|
||||
title="Ghostfolio Status"
|
||||
>Status<ion-icon class="ml-1" name="open-outline"
|
||||
/></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="h6 mt-2" i18n>Community</div>
|
||||
<ul class="list-unstyled">
|
||||
<li>
|
||||
<a
|
||||
class="align-items-baseline d-flex"
|
||||
href="https://github.com/ghostfolio/ghostfolio"
|
||||
target="_blank"
|
||||
title="Find Ghostfolio on GitHub"
|
||||
>GitHub<ion-icon class="ml-1" name="open-outline"
|
||||
/></a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class="align-items-baseline d-flex"
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
target="_blank"
|
||||
title="Join the Ghostfolio Slack community"
|
||||
>Slack<ion-icon class="ml-1" name="open-outline"
|
||||
/></a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class="align-items-baseline d-flex"
|
||||
href="https://twitter.com/ghostfolio_"
|
||||
target="_blank"
|
||||
title="Follow Ghostfolio on X (formerly Twitter)"
|
||||
>X (formerly Twitter)<ion-icon class="ml-1" name="open-outline"
|
||||
/></a>
|
||||
</li>
|
||||
<li> </li>
|
||||
<li>
|
||||
<a href="../de" title="Ghostfolio in Deutsch">Deutsch</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="../en" title="Ghostfolio in English">English</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="../es" title="Ghostfolio in Español">Español</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="../fr" title="Ghostfolio en Français">Français</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="../it" title="Ghostfolio in Italiano">Italiano</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="../nl" title="Ghostfolio in Nederlands">Nederlands</a>
|
||||
</li>
|
||||
<!--
|
||||
@if (showFooter) {
|
||||
<footer class="d-flex justify-content-center py-4 w-100">
|
||||
<div class="container">
|
||||
<div class="mb-3 row">
|
||||
<div class="col-sm">
|
||||
<a [routerLink]="['/']"><gf-logo /></a>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="h6 mt-2" i18n>Personal Finance</div>
|
||||
<ul class="list-unstyled">
|
||||
@if (hasPermissionToAccessFearAndGreedIndex) {
|
||||
<li>
|
||||
<a i18n [routerLink]="routerLinkMarkets">Markets</a>
|
||||
</li>
|
||||
}
|
||||
<li><a i18n [routerLink]="routerLinkResources">Resources</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="h6 mt-2">Ghostfolio</div>
|
||||
<ul class="list-unstyled">
|
||||
<li><a i18n [routerLink]="routerLinkAbout">About</a></li>
|
||||
@if (hasPermissionForSubscription) {
|
||||
<li>
|
||||
<a i18n [routerLink]="['/blog']">Blog</a>
|
||||
</li>
|
||||
}
|
||||
<li>
|
||||
<a href="../pl" title="Ghostfolio in Polski">Polski</a>
|
||||
<a i18n [routerLink]="routerLinkAboutChangelog">Changelog</a>
|
||||
</li>
|
||||
-->
|
||||
<li>
|
||||
<a href="../pt" title="Ghostfolio in Português">Português</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="../tr" title="Ghostfolio in Türkçe">Türkçe</a>
|
||||
</li>
|
||||
<!--
|
||||
<li><a i18n [routerLink]="routerLinkFeatures">Features</a></li>
|
||||
@if (hasPermissionForSubscription) {
|
||||
<li>
|
||||
<a i18n [routerLink]="routerLinkFaq"
|
||||
>Frequently Asked Questions (FAQ)</a
|
||||
>
|
||||
</li>
|
||||
}
|
||||
<li>
|
||||
<a href="../zh" title="Ghostfolio in Chinese">Chinese</a>
|
||||
<a i18n [routerLink]="routerLinkAboutLicense">License</a>
|
||||
</li>
|
||||
-->
|
||||
</ul>
|
||||
@if (hasPermissionForStatistics) {
|
||||
<li>
|
||||
<a [routerLink]="['/open']">Open Startup</a>
|
||||
</li>
|
||||
}
|
||||
@if (hasPermissionForSubscription) {
|
||||
<li>
|
||||
<a i18n [routerLink]="routerLinkPricing">Pricing</a>
|
||||
</li>
|
||||
}
|
||||
@if (hasPermissionForSubscription) {
|
||||
<li>
|
||||
<a i18n [routerLink]="routerLinkAboutPrivacyPolicy"
|
||||
>Privacy Policy</a
|
||||
>
|
||||
</li>
|
||||
}
|
||||
@if (hasPermissionForSubscription) {
|
||||
<li>
|
||||
<a
|
||||
class="align-items-baseline d-flex"
|
||||
href="https://status.ghostfol.io"
|
||||
target="_blank"
|
||||
title="Ghostfolio Status"
|
||||
>Status<ion-icon class="ml-1" name="open-outline"
|
||||
/></a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="h6 mt-2" i18n>Community</div>
|
||||
<ul class="list-unstyled">
|
||||
<li>
|
||||
<a
|
||||
class="align-items-baseline d-flex"
|
||||
href="https://github.com/ghostfolio/ghostfolio"
|
||||
target="_blank"
|
||||
title="Find Ghostfolio on GitHub"
|
||||
>GitHub<ion-icon class="ml-1" name="open-outline"
|
||||
/></a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class="align-items-baseline d-flex"
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
target="_blank"
|
||||
title="Join the Ghostfolio Slack community"
|
||||
>Slack<ion-icon class="ml-1" name="open-outline"
|
||||
/></a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class="align-items-baseline d-flex"
|
||||
href="https://x.com/ghostfolio_"
|
||||
target="_blank"
|
||||
title="Follow Ghostfolio on X (formerly Twitter)"
|
||||
>X (formerly Twitter)<ion-icon class="ml-1" name="open-outline"
|
||||
/></a>
|
||||
</li>
|
||||
<li> </li>
|
||||
<li>
|
||||
<a href="../de" title="Ghostfolio in Deutsch">Deutsch</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="../en" title="Ghostfolio in English">English</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="../es" title="Ghostfolio in Español">Español</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="../fr" title="Ghostfolio en Français">Français</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="../it" title="Ghostfolio in Italiano">Italiano</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="../nl" title="Ghostfolio in Nederlands">Nederlands</a>
|
||||
</li>
|
||||
<!--
|
||||
<li>
|
||||
<a href="../pl" title="Ghostfolio in Polski">Polski</a>
|
||||
</li>
|
||||
-->
|
||||
<li>
|
||||
<a href="../pt" title="Ghostfolio in Português">Português</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="../tr" title="Ghostfolio in Türkçe">Türkçe</a>
|
||||
</li>
|
||||
<!--
|
||||
<li>
|
||||
<a href="../zh" title="Ghostfolio in Chinese">Chinese</a>
|
||||
</li>
|
||||
-->
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row text-center">
|
||||
<div class="col">
|
||||
© 2021 - {{ currentYear }}
|
||||
<a href="https://ghostfol.io">Ghostfolio</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row text-center text-muted">
|
||||
<div class="col">
|
||||
<small i18n
|
||||
>The risk of loss in trading can be substantial. It is not advisable
|
||||
to invest money you may need in the short term.</small
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row text-center">
|
||||
<div class="col">
|
||||
© 2021 - {{ currentYear }} <a href="https://ghostfol.io">Ghostfolio</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row text-center text-muted">
|
||||
<div class="col">
|
||||
<small i18n
|
||||
>The risk of loss in trading can be substantial. It is not advisable
|
||||
to invest money you may need in the short term.</small
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</footer>
|
||||
}
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { GfLogoComponent } from '@ghostfolio/ui/logo';
|
||||
|
||||
import { Platform } from '@angular/cdk/platform';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import {
|
||||
provideHttpClient,
|
||||
withInterceptorsFromDi
|
||||
} from '@angular/common/http';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
@ -45,7 +48,6 @@ export function NgxStripeFactory(): string {
|
||||
GfHeaderModule,
|
||||
GfLogoComponent,
|
||||
GfSubscriptionInterstitialDialogModule,
|
||||
HttpClientModule,
|
||||
MarkdownModule.forRoot(),
|
||||
MatAutocompleteModule,
|
||||
MatChipsModule,
|
||||
@ -63,6 +65,7 @@ export function NgxStripeFactory(): string {
|
||||
authInterceptorProviders,
|
||||
httpResponseInterceptorProviders,
|
||||
LanguageService,
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
{
|
||||
provide: DateAdapter,
|
||||
useClass: CustomDateAdapter,
|
||||
|
@ -1,67 +1,71 @@
|
||||
<table class="gf-table w-100" mat-table [dataSource]="dataSource">
|
||||
<ng-container matColumnDef="alias">
|
||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Alias</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
{{ element.alias }}
|
||||
</td>
|
||||
</ng-container>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="gf-table w-100" mat-table [dataSource]="dataSource">
|
||||
<ng-container matColumnDef="alias">
|
||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Alias</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
{{ element.alias }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="grantee">
|
||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Grantee</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
{{ element.grantee }}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="grantee">
|
||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Grantee</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
{{ element.grantee }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="type">
|
||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Permission</th>
|
||||
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell>
|
||||
<div class="align-items-center d-flex">
|
||||
@if (element.permissions.includes('READ')) {
|
||||
<ion-icon class="mr-1" name="lock-open-outline" />
|
||||
<ng-container i18n>View</ng-container>
|
||||
} @else if (element.permissions.includes('READ_RESTRICTED')) {
|
||||
<ion-icon class="mr-1" name="lock-closed-outline" />
|
||||
<ng-container i18n>Restricted view</ng-container>
|
||||
<ng-container matColumnDef="type">
|
||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Permission</th>
|
||||
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell>
|
||||
<div class="align-items-center d-flex">
|
||||
@if (element.permissions.includes('READ')) {
|
||||
<ion-icon class="mr-1" name="lock-open-outline" />
|
||||
<ng-container i18n>View</ng-container>
|
||||
} @else if (element.permissions.includes('READ_RESTRICTED')) {
|
||||
<ion-icon class="mr-1" name="lock-closed-outline" />
|
||||
<ng-container i18n>Restricted view</ng-container>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="details">
|
||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Details</th>
|
||||
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell>
|
||||
@if (element.type === 'PUBLIC') {
|
||||
<div class="align-items-center d-flex">
|
||||
<ion-icon class="mr-1" name="link-outline" />
|
||||
<a
|
||||
href="{{ baseUrl }}/{{ defaultLanguageCode }}/p/{{ element.id }}"
|
||||
target="_blank"
|
||||
>{{ baseUrl }}/{{ defaultLanguageCode }}/p/{{ element.id }}</a
|
||||
>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="details">
|
||||
<th *matHeaderCellDef class="px-1" i18n mat-header-cell>Details</th>
|
||||
<td *matCellDef="let element" class="px-1 text-nowrap" mat-cell>
|
||||
<div *ngIf="element.type === 'PUBLIC'" class="align-items-center d-flex">
|
||||
<ion-icon class="mr-1" name="link-outline" />
|
||||
<a
|
||||
href="{{ baseUrl }}/{{ defaultLanguageCode }}/p/{{ element.id }}"
|
||||
target="_blank"
|
||||
>{{ baseUrl }}/{{ defaultLanguageCode }}/p/{{ element.id }}</a
|
||||
<ng-container matColumnDef="actions" stickyEnd>
|
||||
<th *matHeaderCellDef class="px-1 text-center" mat-header-cell></th>
|
||||
|
||||
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
||||
<button
|
||||
class="mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
[matMenuTriggerFor]="transactionMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions" stickyEnd>
|
||||
<th *matHeaderCellDef class="px-1 text-center" mat-header-cell></th>
|
||||
|
||||
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
||||
<button
|
||||
class="mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
[matMenuTriggerFor]="transactionMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<ion-icon name="ellipsis-horizontal" />
|
||||
</button>
|
||||
<mat-menu #transactionMenu="matMenu" xPosition="before">
|
||||
<button mat-menu-item (click)="onDeleteAccess(element.id)">
|
||||
<ng-container i18n>Revoke</ng-container>
|
||||
<ion-icon name="ellipsis-horizontal" />
|
||||
</button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
</ng-container>
|
||||
<mat-menu #transactionMenu="matMenu" xPosition="before">
|
||||
<button mat-menu-item (click)="onDeleteAccess(element.id)">
|
||||
<ng-container i18n>Revoke</ng-container>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
||||
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
|
||||
</table>
|
||||
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
||||
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
@ -1,296 +1,322 @@
|
||||
<div *ngIf="showActions" class="d-flex justify-content-end">
|
||||
<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" />
|
||||
<ng-container i18n>Transfer Cash Balance</ng-container>...
|
||||
</button>
|
||||
@if (showActions) {
|
||||
<div class="d-flex justify-content-end">
|
||||
<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" />
|
||||
<ng-container i18n>Transfer Cash Balance</ng-container>...
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="gf-table w-100" mat-table matSort [dataSource]="dataSource">
|
||||
<ng-container matColumnDef="status">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="d-none d-lg-table-cell px-1"
|
||||
mat-header-cell
|
||||
></th>
|
||||
<td
|
||||
*matCellDef="let element"
|
||||
class="d-none d-lg-table-cell px-1"
|
||||
mat-cell
|
||||
>
|
||||
<div class="d-flex justify-content-center">
|
||||
@if (element.isExcluded) {
|
||||
<ion-icon name="eye-off-outline" />
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
*matFooterCellDef
|
||||
class="d-none d-lg-table-cell px-1"
|
||||
mat-footer-cell
|
||||
></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="account">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header="name">
|
||||
<ng-container i18n>Name</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
@if (element.Platform?.url) {
|
||||
<gf-asset-profile-icon
|
||||
class="d-inline d-sm-none mr-1"
|
||||
[tooltip]="element.Platform?.name"
|
||||
[url]="element.Platform?.url"
|
||||
/>
|
||||
}
|
||||
<span>{{ element.name }}</span>
|
||||
</td>
|
||||
<td *matFooterCellDef class="px-1" i18n mat-footer-cell>Total</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="currency">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="d-none d-lg-table-cell px-1"
|
||||
mat-header-cell
|
||||
mat-sort-header
|
||||
>
|
||||
<ng-container i18n>Currency</ng-container>
|
||||
</th>
|
||||
<td
|
||||
*matCellDef="let element"
|
||||
class="d-none d-lg-table-cell px-1"
|
||||
mat-cell
|
||||
>
|
||||
{{ element.currency }}
|
||||
</td>
|
||||
<td *matFooterCellDef class="d-none d-lg-table-cell px-1" mat-footer-cell>
|
||||
{{ baseCurrency }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="platform">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="d-none d-lg-table-cell px-1"
|
||||
mat-header-cell
|
||||
mat-sort-header="Platform.name"
|
||||
>
|
||||
<ng-container i18n>Platform</ng-container>
|
||||
</th>
|
||||
<td
|
||||
*matCellDef="let element"
|
||||
class="d-none d-lg-table-cell px-1"
|
||||
mat-cell
|
||||
>
|
||||
<div class="d-flex">
|
||||
@if (element.Platform?.url) {
|
||||
<gf-asset-profile-icon
|
||||
class="mr-1"
|
||||
[tooltip]="element.Platform?.name"
|
||||
[url]="element.Platform?.url"
|
||||
/>
|
||||
}
|
||||
<span>{{ element.Platform?.name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
*matFooterCellDef
|
||||
class="d-none d-lg-table-cell px-1"
|
||||
mat-footer-cell
|
||||
></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="transactions">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="justify-content-end px-1"
|
||||
mat-header-cell
|
||||
mat-sort-header="transactionCount"
|
||||
>
|
||||
<span class="d-block d-sm-none">#</span>
|
||||
<span class="d-none d-sm-block" i18n>Activities</span>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
||||
{{ element.transactionCount }}
|
||||
</td>
|
||||
<td *matFooterCellDef class="px-1 text-right" mat-footer-cell>
|
||||
{{ transactionCount }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="balance">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="d-none d-lg-table-cell justify-content-end px-1"
|
||||
mat-header-cell
|
||||
mat-sort-header
|
||||
>
|
||||
<ng-container i18n>Cash Balance</ng-container>
|
||||
</th>
|
||||
<td
|
||||
*matCellDef="let element"
|
||||
class="d-none d-lg-table-cell px-1 text-right"
|
||||
mat-cell
|
||||
>
|
||||
<gf-value
|
||||
class="d-inline-block justify-content-end"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="element.balance"
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
*matFooterCellDef
|
||||
class="d-none d-lg-table-cell px-1 text-right"
|
||||
mat-footer-cell
|
||||
>
|
||||
<gf-value
|
||||
class="d-inline-block justify-content-end"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="totalBalanceInBaseCurrency"
|
||||
/>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="d-none d-lg-table-cell justify-content-end px-1"
|
||||
mat-header-cell
|
||||
mat-sort-header
|
||||
>
|
||||
<ng-container i18n>Value</ng-container>
|
||||
</th>
|
||||
<td
|
||||
*matCellDef="let element"
|
||||
class="d-none d-lg-table-cell px-1 text-right"
|
||||
mat-cell
|
||||
>
|
||||
<gf-value
|
||||
class="d-inline-block justify-content-end"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="element.value"
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
*matFooterCellDef
|
||||
class="d-none d-lg-table-cell px-1 text-right"
|
||||
mat-footer-cell
|
||||
>
|
||||
<gf-value
|
||||
class="d-inline-block justify-content-end"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="totalValueInBaseCurrency"
|
||||
/>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="valueInBaseCurrency">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="d-lg-none d-xl-none px-1 text-right"
|
||||
mat-header-cell
|
||||
mat-sort-header
|
||||
>
|
||||
<ng-container i18n>Value</ng-container>
|
||||
</th>
|
||||
<td
|
||||
*matCellDef="let element"
|
||||
class="d-lg-none d-xl-none px-1 text-right"
|
||||
mat-cell
|
||||
>
|
||||
<gf-value
|
||||
class="d-inline-block justify-content-end"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="element.valueInBaseCurrency"
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
*matFooterCellDef
|
||||
class="d-lg-none d-xl-none px-1 text-right"
|
||||
mat-footer-cell
|
||||
>
|
||||
<gf-value
|
||||
class="d-inline-block justify-content-end"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="totalValueInBaseCurrency"
|
||||
/>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="comment">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="d-none d-lg-table-cell px-1"
|
||||
mat-header-cell
|
||||
></th>
|
||||
<td
|
||||
*matCellDef="let element"
|
||||
class="d-none d-lg-table-cell px-1"
|
||||
mat-cell
|
||||
>
|
||||
@if (element.comment) {
|
||||
<button
|
||||
class="mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
title="Note"
|
||||
(click)="onOpenComment(element.comment); $event.stopPropagation()"
|
||||
>
|
||||
<ion-icon name="document-text-outline" />
|
||||
</button>
|
||||
}
|
||||
</td>
|
||||
<td
|
||||
*matFooterCellDef
|
||||
class="d-none d-lg-table-cell px-1"
|
||||
mat-footer-cell
|
||||
></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions" stickyEnd>
|
||||
<th *matHeaderCellDef class="px-1 text-center" i18n mat-header-cell></th>
|
||||
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
||||
<button
|
||||
class="mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
[matMenuTriggerFor]="accountMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<ion-icon name="ellipsis-horizontal" />
|
||||
</button>
|
||||
<mat-menu #accountMenu="matMenu" xPosition="before">
|
||||
<button mat-menu-item (click)="onUpdateAccount(element)">
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="create-outline" />
|
||||
<span i18n>Edit</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
[disabled]="element.transactionCount > 0"
|
||||
(click)="onDeleteAccount(element.id)"
|
||||
>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="trash-outline" />
|
||||
<span i18n>Delete</span>
|
||||
</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
|
||||
</ng-container>
|
||||
|
||||
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
||||
<tr
|
||||
*matRowDef="let row; columns: displayedColumns"
|
||||
mat-row
|
||||
[ngClass]="{
|
||||
'cursor-pointer': hasPermissionToOpenDetails
|
||||
}"
|
||||
(click)="onOpenAccountDetailDialog(row.id)"
|
||||
></tr>
|
||||
<tr
|
||||
*matFooterRowDef="displayedColumns"
|
||||
mat-footer-row
|
||||
[ngClass]="{ 'd-none': isLoading || !showFooter }"
|
||||
></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<table class="gf-table w-100" mat-table matSort [dataSource]="dataSource">
|
||||
<ng-container matColumnDef="status">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="d-none d-lg-table-cell px-1"
|
||||
mat-header-cell
|
||||
></th>
|
||||
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
|
||||
<div class="d-flex justify-content-center">
|
||||
<ion-icon *ngIf="element.isExcluded" name="eye-off-outline" />
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
*matFooterCellDef
|
||||
class="d-none d-lg-table-cell px-1"
|
||||
mat-footer-cell
|
||||
></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="account">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header="name">
|
||||
<ng-container i18n>Name</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
<gf-asset-profile-icon
|
||||
*ngIf="element.Platform?.url"
|
||||
class="d-inline d-sm-none mr-1"
|
||||
[tooltip]="element.Platform?.name"
|
||||
[url]="element.Platform?.url"
|
||||
/>
|
||||
<span>{{ element.name }}</span>
|
||||
</td>
|
||||
<td *matFooterCellDef class="px-1" i18n mat-footer-cell>Total</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="currency">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="d-none d-lg-table-cell px-1"
|
||||
mat-header-cell
|
||||
mat-sort-header
|
||||
>
|
||||
<ng-container i18n>Currency</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
|
||||
{{ element.currency }}
|
||||
</td>
|
||||
<td *matFooterCellDef class="d-none d-lg-table-cell px-1" mat-footer-cell>
|
||||
{{ baseCurrency }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="platform">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="d-none d-lg-table-cell px-1"
|
||||
mat-header-cell
|
||||
mat-sort-header="Platform.name"
|
||||
>
|
||||
<ng-container i18n>Platform</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
|
||||
<div class="d-flex">
|
||||
<gf-asset-profile-icon
|
||||
*ngIf="element.Platform?.url"
|
||||
class="mr-1"
|
||||
[tooltip]="element.Platform?.name"
|
||||
[url]="element.Platform?.url"
|
||||
/>
|
||||
<span>{{ element.Platform?.name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
*matFooterCellDef
|
||||
class="d-none d-lg-table-cell px-1"
|
||||
mat-footer-cell
|
||||
></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="transactions">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="justify-content-end px-1"
|
||||
mat-header-cell
|
||||
mat-sort-header="transactionCount"
|
||||
>
|
||||
<span class="d-block d-sm-none">#</span>
|
||||
<span class="d-none d-sm-block" i18n>Activities</span>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
||||
{{ element.transactionCount }}
|
||||
</td>
|
||||
<td *matFooterCellDef class="px-1 text-right" mat-footer-cell>
|
||||
{{ transactionCount }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="balance">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="d-none d-lg-table-cell justify-content-end px-1"
|
||||
mat-header-cell
|
||||
mat-sort-header
|
||||
>
|
||||
<ng-container i18n>Cash Balance</ng-container>
|
||||
</th>
|
||||
<td
|
||||
*matCellDef="let element"
|
||||
class="d-none d-lg-table-cell px-1 text-right"
|
||||
mat-cell
|
||||
>
|
||||
<gf-value
|
||||
class="d-inline-block justify-content-end"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="element.balance"
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
*matFooterCellDef
|
||||
class="d-none d-lg-table-cell px-1 text-right"
|
||||
mat-footer-cell
|
||||
>
|
||||
<gf-value
|
||||
class="d-inline-block justify-content-end"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="totalBalanceInBaseCurrency"
|
||||
/>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="value">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="d-none d-lg-table-cell justify-content-end px-1"
|
||||
mat-header-cell
|
||||
mat-sort-header
|
||||
>
|
||||
<ng-container i18n>Value</ng-container>
|
||||
</th>
|
||||
<td
|
||||
*matCellDef="let element"
|
||||
class="d-none d-lg-table-cell px-1 text-right"
|
||||
mat-cell
|
||||
>
|
||||
<gf-value
|
||||
class="d-inline-block justify-content-end"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="element.value"
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
*matFooterCellDef
|
||||
class="d-none d-lg-table-cell px-1 text-right"
|
||||
mat-footer-cell
|
||||
>
|
||||
<gf-value
|
||||
class="d-inline-block justify-content-end"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="totalValueInBaseCurrency"
|
||||
/>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="valueInBaseCurrency">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="d-lg-none d-xl-none px-1 text-right"
|
||||
mat-header-cell
|
||||
mat-sort-header
|
||||
>
|
||||
<ng-container i18n>Value</ng-container>
|
||||
</th>
|
||||
<td
|
||||
*matCellDef="let element"
|
||||
class="d-lg-none d-xl-none px-1 text-right"
|
||||
mat-cell
|
||||
>
|
||||
<gf-value
|
||||
class="d-inline-block justify-content-end"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="element.valueInBaseCurrency"
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
*matFooterCellDef
|
||||
class="d-lg-none d-xl-none px-1 text-right"
|
||||
mat-footer-cell
|
||||
>
|
||||
<gf-value
|
||||
class="d-inline-block justify-content-end"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="totalValueInBaseCurrency"
|
||||
/>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="comment">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="d-none d-lg-table-cell px-1"
|
||||
mat-header-cell
|
||||
></th>
|
||||
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
|
||||
<button
|
||||
*ngIf="element.comment"
|
||||
class="mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
title="Note"
|
||||
(click)="onOpenComment(element.comment); $event.stopPropagation()"
|
||||
>
|
||||
<ion-icon name="document-text-outline" />
|
||||
</button>
|
||||
</td>
|
||||
<td
|
||||
*matFooterCellDef
|
||||
class="d-none d-lg-table-cell px-1"
|
||||
mat-footer-cell
|
||||
></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions" stickyEnd>
|
||||
<th *matHeaderCellDef class="px-1 text-center" i18n mat-header-cell></th>
|
||||
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
||||
<button
|
||||
class="mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
[matMenuTriggerFor]="accountMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<ion-icon name="ellipsis-horizontal" />
|
||||
</button>
|
||||
<mat-menu #accountMenu="matMenu" xPosition="before">
|
||||
<button mat-menu-item (click)="onUpdateAccount(element)">
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="create-outline" />
|
||||
<span i18n>Edit</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
[disabled]="element.transactionCount > 0"
|
||||
(click)="onDeleteAccount(element.id)"
|
||||
>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="trash-outline" />
|
||||
<span i18n>Delete</span>
|
||||
</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
|
||||
</ng-container>
|
||||
|
||||
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
||||
<tr
|
||||
*matRowDef="let row; columns: displayedColumns"
|
||||
mat-row
|
||||
[ngClass]="{
|
||||
'cursor-pointer': hasPermissionToOpenDetails
|
||||
@if (isLoading) {
|
||||
<ngx-skeleton-loader
|
||||
animation="pulse"
|
||||
class="px-4 py-3"
|
||||
[theme]="{
|
||||
height: '1.5rem',
|
||||
width: '100%'
|
||||
}"
|
||||
(click)="onOpenAccountDetailDialog(row.id)"
|
||||
></tr>
|
||||
<tr
|
||||
*matFooterRowDef="displayedColumns"
|
||||
mat-footer-row
|
||||
[ngClass]="{ 'd-none': isLoading || !showFooter }"
|
||||
></tr>
|
||||
</table>
|
||||
|
||||
<ngx-skeleton-loader
|
||||
*ngIf="isLoading"
|
||||
animation="pulse"
|
||||
class="px-4 py-3"
|
||||
[theme]="{
|
||||
height: '1.5rem',
|
||||
width: '100%'
|
||||
}"
|
||||
/>
|
||||
/>
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
.mat-mdc-table {
|
||||
.gf-table {
|
||||
th {
|
||||
::ng-deep {
|
||||
.mat-sort-header-container {
|
||||
|
@ -5,11 +5,14 @@
|
||||
<mat-form-field appearance="outline" class="w-100 without-hint">
|
||||
<mat-select formControlName="status">
|
||||
<mat-option />
|
||||
<mat-option
|
||||
*ngFor="let statusFilterOption of statusFilterOptions"
|
||||
[value]="statusFilterOption"
|
||||
>{{ statusFilterOption }}</mat-option
|
||||
>
|
||||
@for (
|
||||
statusFilterOption of statusFilterOptions;
|
||||
track statusFilterOption
|
||||
) {
|
||||
<mat-option [value]="statusFilterOption">{{
|
||||
statusFilterOption
|
||||
}}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
@ -28,15 +31,11 @@
|
||||
<ng-container i18n>Type</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1 py-2" mat-cell>
|
||||
<ng-container *ngIf="element.name === 'GATHER_ASSET_PROFILE'" i18n>
|
||||
Asset Profile
|
||||
</ng-container>
|
||||
<ng-container
|
||||
*ngIf="element.name === 'GATHER_HISTORICAL_MARKET_DATA'"
|
||||
i18n
|
||||
>
|
||||
Historical Market Data
|
||||
</ng-container>
|
||||
@if (element.name === 'GATHER_ASSET_PROFILE') {
|
||||
<ng-container i18n>Asset Profile</ng-container>
|
||||
} @else if (element.name === 'GATHER_HISTORICAL_MARKET_DATA') {
|
||||
<ng-container i18n>Historical Market Data</ng-container>
|
||||
}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@ -109,37 +108,29 @@
|
||||
<ng-container i18n>Status</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1 py-2" mat-cell>
|
||||
<ion-icon
|
||||
*ngIf="element.state === 'active'"
|
||||
class="h6 mb-0"
|
||||
name="play-outline"
|
||||
/>
|
||||
<ion-icon
|
||||
*ngIf="element.state === 'completed'"
|
||||
class="h6 mb-0 text-success"
|
||||
name="checkmark-circle-outline"
|
||||
/>
|
||||
<ion-icon
|
||||
*ngIf="element.state === 'delayed'"
|
||||
class="h6 mb-0"
|
||||
name="time-outline"
|
||||
[ngClass]="{ 'text-danger': element.stacktrace?.length > 0 }"
|
||||
/>
|
||||
<ion-icon
|
||||
*ngIf="element.state === 'failed'"
|
||||
class="h6 mb-0 text-danger"
|
||||
name="alert-circle-outline"
|
||||
/>
|
||||
<ion-icon
|
||||
*ngIf="element.state === 'paused'"
|
||||
class="h6 mb-0"
|
||||
name="pause-outline"
|
||||
/>
|
||||
<ion-icon
|
||||
*ngIf="element.state === 'waiting'"
|
||||
class="h6 mb-0"
|
||||
name="cafe-outline"
|
||||
/>
|
||||
@if (element.state === 'active') {
|
||||
<ion-icon class="h6 mb-0" name="play-outline" />
|
||||
} @else if (element.state === 'completed') {
|
||||
<ion-icon
|
||||
class="h6 mb-0 text-success"
|
||||
name="checkmark-circle-outline"
|
||||
/>
|
||||
} @else if (element.state === 'delayed') {
|
||||
<ion-icon
|
||||
class="h6 mb-0"
|
||||
name="time-outline"
|
||||
[ngClass]="{ 'text-danger': element.stacktrace?.length > 0 }"
|
||||
/>
|
||||
} @else if (element.state === 'failed') {
|
||||
<ion-icon
|
||||
class="h6 mb-0 text-danger"
|
||||
name="alert-circle-outline"
|
||||
/>
|
||||
} @else if (element.state === 'paused') {
|
||||
<ion-icon class="h6 mb-0" name="pause-outline" />
|
||||
} @else if (element.state === 'waiting') {
|
||||
<ion-icon class="h6 mb-0" name="cafe-outline" />
|
||||
}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
|
@ -9,35 +9,38 @@
|
||||
[showYAxis]="true"
|
||||
[symbol]="symbol"
|
||||
/>
|
||||
<div *ngFor="let itemByMonth of marketDataByMonth | keyvalue" class="d-flex">
|
||||
<div class="date px-1 text-nowrap">{{ itemByMonth.key }}</div>
|
||||
<div class="align-items-center d-flex flex-grow-1 px-1">
|
||||
<div
|
||||
*ngFor="let dayItem of days; let i = index"
|
||||
class="day"
|
||||
[ngClass]="{
|
||||
'cursor-pointer valid': isDateOfInterest(
|
||||
itemByMonth.key + '-' + (i + 1 < 10 ? '0' + (i + 1) : i + 1)
|
||||
),
|
||||
available:
|
||||
marketDataByMonth[itemByMonth.key][
|
||||
i + 1 < 10 ? '0' + (i + 1) : i + 1
|
||||
]?.marketPrice,
|
||||
today: isToday(
|
||||
itemByMonth.key + '-' + (i + 1 < 10 ? '0' + (i + 1) : i + 1)
|
||||
)
|
||||
}"
|
||||
[title]="
|
||||
(itemByMonth.key + '-' + (i + 1 < 10 ? '0' + (i + 1) : i + 1)
|
||||
| date: defaultDateFormat) ?? ''
|
||||
"
|
||||
(click)="
|
||||
onOpenMarketDataDetail({
|
||||
day: i + 1 < 10 ? '0' + (i + 1) : i + 1,
|
||||
yearMonth: itemByMonth.key
|
||||
})
|
||||
"
|
||||
></div>
|
||||
@for (itemByMonth of marketDataByMonth | keyvalue; track itemByMonth) {
|
||||
<div class="d-flex">
|
||||
<div class="date px-1 text-nowrap">{{ itemByMonth.key }}</div>
|
||||
<div class="align-items-center d-flex flex-grow-1 px-1">
|
||||
@for (dayItem of days; track dayItem; let i = $index) {
|
||||
<div
|
||||
class="day"
|
||||
[ngClass]="{
|
||||
'cursor-pointer valid': isDateOfInterest(
|
||||
itemByMonth.key + '-' + (i + 1 < 10 ? '0' + (i + 1) : i + 1)
|
||||
),
|
||||
available:
|
||||
marketDataByMonth[itemByMonth.key][
|
||||
i + 1 < 10 ? '0' + (i + 1) : i + 1
|
||||
]?.marketPrice,
|
||||
today: isToday(
|
||||
itemByMonth.key + '-' + (i + 1 < 10 ? '0' + (i + 1) : i + 1)
|
||||
)
|
||||
}"
|
||||
[title]="
|
||||
(itemByMonth.key + '-' + (i + 1 < 10 ? '0' + (i + 1) : i + 1)
|
||||
| date: defaultDateFormat) ?? ''
|
||||
"
|
||||
(click)="
|
||||
onOpenMarketDataDetail({
|
||||
day: i + 1 < 10 ? '0' + (i + 1) : i + 1,
|
||||
yearMonth: itemByMonth.key
|
||||
})
|
||||
"
|
||||
></div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
@ -10,6 +10,7 @@ import { Filter, UniqueAsset, User } from '@ghostfolio/common/interfaces';
|
||||
import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface';
|
||||
import { translate } from '@ghostfolio/ui/i18n';
|
||||
|
||||
import { SelectionModel } from '@angular/cdk/collections';
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectionStrategy,
|
||||
@ -68,6 +69,11 @@ export class AdminMarketDataComponent
|
||||
};
|
||||
})
|
||||
.concat([
|
||||
{
|
||||
id: 'BENCHMARKS',
|
||||
label: $localize`Benchmarks`,
|
||||
type: <Filter['type']>'PRESET_ID'
|
||||
},
|
||||
{
|
||||
id: 'CURRENCIES',
|
||||
label: $localize`Currencies`,
|
||||
@ -92,6 +98,7 @@ export class AdminMarketDataComponent
|
||||
public defaultDateFormat: string;
|
||||
public deviceType: string;
|
||||
public displayedColumns = [
|
||||
'select',
|
||||
'nameWithSymbol',
|
||||
'dataSource',
|
||||
'assetClass',
|
||||
@ -110,13 +117,14 @@ export class AdminMarketDataComponent
|
||||
public isUUID = isUUID;
|
||||
public placeholder = '';
|
||||
public pageSize = DEFAULT_PAGE_SIZE;
|
||||
public selection: SelectionModel<Partial<SymbolProfile>>;
|
||||
public totalItems = 0;
|
||||
public user: User;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor(
|
||||
private adminMarketDataService: AdminMarketDataService,
|
||||
public adminMarketDataService: AdminMarketDataService,
|
||||
private adminService: AdminService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
@ -183,6 +191,8 @@ export class AdminMarketDataComponent
|
||||
|
||||
this.benchmarks = benchmarks;
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
|
||||
this.selection = new SelectionModel(true);
|
||||
}
|
||||
|
||||
public onChangePage(page: PageEvent) {
|
||||
@ -193,8 +203,16 @@ export class AdminMarketDataComponent
|
||||
});
|
||||
}
|
||||
|
||||
public onDeleteProfileData({ dataSource, symbol }: UniqueAsset) {
|
||||
this.adminMarketDataService.deleteProfileData({ dataSource, symbol });
|
||||
public onDeleteAssetProfile({ dataSource, symbol }: UniqueAsset) {
|
||||
this.adminMarketDataService.deleteAssetProfile({ dataSource, symbol });
|
||||
}
|
||||
|
||||
public onDeleteAssetProfiles() {
|
||||
this.adminMarketDataService.deleteAssetProfiles(
|
||||
this.selection.selected.map(({ dataSource, symbol }) => {
|
||||
return { dataSource, symbol };
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public onGather7Days() {
|
||||
@ -281,6 +299,8 @@ export class AdminMarketDataComponent
|
||||
this.placeholder =
|
||||
this.activeFilters.length <= 0 ? $localize`Filter by...` : '';
|
||||
|
||||
this.selection.clear();
|
||||
|
||||
this.adminService
|
||||
.fetchAdminMarketData({
|
||||
sortColumn,
|
||||
|
@ -11,206 +11,240 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table
|
||||
class="gf-table w-100"
|
||||
mat-table
|
||||
matSort
|
||||
matSortActive="symbol"
|
||||
matSortDirection="asc"
|
||||
[dataSource]="dataSource"
|
||||
>
|
||||
<ng-container matColumnDef="symbol">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||
<ng-container i18n>Symbol</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
{{ element.symbol }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="nameWithSymbol">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="px-1"
|
||||
mat-header-cell
|
||||
mat-sort-header="symbol"
|
||||
>
|
||||
<ng-container i18n>Name</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="line-height-1 px-1" mat-cell>
|
||||
<div class="text-truncate">{{ element.name }}</div>
|
||||
<div *ngIf="!isUUID(element.symbol)">
|
||||
<small class="text-muted">{{ element.symbol | gfSymbol }}</small>
|
||||
</div>
|
||||
</td>
|
||||
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="dataSource">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||
<ng-container i18n>Data Source</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
{{ element.dataSource }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="assetClass">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||
<ng-container i18n>Asset Class</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
{{ element.assetClass }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="assetSubClass">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||
<ng-container i18n>Asset Sub Class</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
{{ element.assetSubClass }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="date">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell>
|
||||
<ng-container i18n>First Activity</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
{{ (element.date | date: defaultDateFormat) ?? '' }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="activitiesCount">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||
<ng-container i18n>Activities Count</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
||||
{{ element.activitiesCount }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="marketDataItemCount">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell>
|
||||
<ng-container i18n>Historical Data</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
||||
{{ element.marketDataItemCount }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="sectorsCount">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell>
|
||||
<ng-container i18n>Sectors Count</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
||||
{{ element.sectorsCount }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="countriesCount">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell>
|
||||
<ng-container i18n>Countries Count</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
||||
{{ element.countriesCount }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="comment">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell></th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
<ion-icon
|
||||
*ngIf="element.comment"
|
||||
class="d-block"
|
||||
name="document-text-outline"
|
||||
/>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions" stickyEnd>
|
||||
<th *matHeaderCellDef class="px-1 text-center" mat-header-cell>
|
||||
<button
|
||||
class="mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
[matMenuTriggerFor]="assetProfilesActionsMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<ion-icon name="ellipsis-vertical" />
|
||||
</button>
|
||||
<mat-menu #assetProfilesActionsMenu="matMenu" xPosition="before">
|
||||
<button mat-menu-item (click)="onGather7Days()">
|
||||
<ng-container i18n>Gather Recent Data</ng-container>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onGatherMax()">
|
||||
<ng-container i18n>Gather All Data</ng-container>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onGatherProfileData()">
|
||||
<ng-container i18n>Gather Profile Data</ng-container>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
||||
<button
|
||||
class="mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
[matMenuTriggerFor]="assetProfileActionsMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<ion-icon name="ellipsis-horizontal" />
|
||||
</button>
|
||||
<mat-menu #assetProfileActionsMenu="matMenu" xPosition="before">
|
||||
<a
|
||||
mat-menu-item
|
||||
[queryParams]="{
|
||||
assetProfileDialog: true,
|
||||
dataSource: element.dataSource,
|
||||
<div class="overflow-x-auto">
|
||||
<table
|
||||
class="gf-table w-100"
|
||||
mat-table
|
||||
matSort
|
||||
matSortActive="symbol"
|
||||
matSortDirection="asc"
|
||||
[dataSource]="dataSource"
|
||||
>
|
||||
<ng-container matColumnDef="select">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell></th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
@if (
|
||||
adminMarketDataService.hasPermissionToDeleteAssetProfile({
|
||||
activitiesCount: element.activitiesCount,
|
||||
isBenchmark: element.isBenchmark,
|
||||
symbol: element.symbol
|
||||
}"
|
||||
[routerLink]="[]"
|
||||
>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="create-outline" />
|
||||
<span i18n>Edit</span>
|
||||
</span>
|
||||
</a>
|
||||
})
|
||||
) {
|
||||
<mat-checkbox
|
||||
color="primary"
|
||||
[checked]="selection.isSelected(element)"
|
||||
(change)="$event ? selection.toggle(element) : null"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
</mat-checkbox>
|
||||
}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="symbol">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||
<ng-container i18n>Symbol</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
{{ element.symbol }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="nameWithSymbol">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="px-1"
|
||||
mat-header-cell
|
||||
mat-sort-header="symbol"
|
||||
>
|
||||
<ng-container i18n>Name</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="line-height-1 px-1" mat-cell>
|
||||
<div class="text-truncate">{{ element.name }}</div>
|
||||
@if (!isUUID(element.symbol)) {
|
||||
<div>
|
||||
<small class="text-muted">{{
|
||||
element.symbol | gfSymbol
|
||||
}}</small>
|
||||
</div>
|
||||
}
|
||||
</td>
|
||||
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="dataSource">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||
<ng-container i18n>Data Source</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
{{ element.dataSource }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="assetClass">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||
<ng-container i18n>Asset Class</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
{{ element.assetClass }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="assetSubClass">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||
<ng-container i18n>Asset Sub Class</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
{{ element.assetSubClass }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="date">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell>
|
||||
<ng-container i18n>First Activity</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
{{ (element.date | date: defaultDateFormat) ?? '' }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="activitiesCount">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
|
||||
<ng-container i18n>Activities Count</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
||||
{{ element.activitiesCount }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="marketDataItemCount">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell>
|
||||
<ng-container i18n>Historical Data</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
||||
{{ element.marketDataItemCount }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="sectorsCount">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell>
|
||||
<ng-container i18n>Sectors Count</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
||||
{{ element.sectorsCount }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="countriesCount">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell>
|
||||
<ng-container i18n>Countries Count</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
||||
{{ element.countriesCount }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="comment">
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell></th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
@if (element.comment) {
|
||||
<ion-icon class="d-block" name="document-text-outline" />
|
||||
}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions" stickyEnd>
|
||||
<th *matHeaderCellDef class="px-1 text-center" mat-header-cell>
|
||||
<button
|
||||
mat-menu-item
|
||||
[disabled]="
|
||||
element.activitiesCount !== 0 ||
|
||||
element.isBenchmark ||
|
||||
element.symbol.startsWith(ghostfolioScraperApiSymbolPrefix)
|
||||
"
|
||||
(click)="
|
||||
onDeleteProfileData({
|
||||
class="mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
[matMenuTriggerFor]="assetProfilesActionsMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<ion-icon name="ellipsis-vertical" />
|
||||
</button>
|
||||
<mat-menu #assetProfilesActionsMenu="matMenu" xPosition="before">
|
||||
<button mat-menu-item (click)="onGather7Days()">
|
||||
<ng-container i18n>Gather Recent Data</ng-container>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onGatherMax()">
|
||||
<ng-container i18n>Gather All Data</ng-container>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onGatherProfileData()">
|
||||
<ng-container i18n>Gather Profile Data</ng-container>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
[disabled]="!selection.hasValue()"
|
||||
(click)="onDeleteAssetProfiles()"
|
||||
>
|
||||
<ng-container i18n>Delete Profiles</ng-container>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
||||
<button
|
||||
class="mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
[matMenuTriggerFor]="assetProfileActionsMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<ion-icon name="ellipsis-horizontal" />
|
||||
</button>
|
||||
<mat-menu #assetProfileActionsMenu="matMenu" xPosition="before">
|
||||
<a
|
||||
mat-menu-item
|
||||
[queryParams]="{
|
||||
assetProfileDialog: true,
|
||||
dataSource: element.dataSource,
|
||||
symbol: element.symbol
|
||||
})
|
||||
"
|
||||
>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="trash-outline" />
|
||||
<span i18n>Delete</span>
|
||||
</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
</ng-container>
|
||||
}"
|
||||
[routerLink]="[]"
|
||||
>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="create-outline" />
|
||||
<span i18n>Edit</span>
|
||||
</span>
|
||||
</a>
|
||||
<button
|
||||
mat-menu-item
|
||||
[disabled]="
|
||||
!adminMarketDataService.hasPermissionToDeleteAssetProfile({
|
||||
activitiesCount: element.activitiesCount,
|
||||
isBenchmark: element.isBenchmark,
|
||||
symbol: element.symbol
|
||||
})
|
||||
"
|
||||
(click)="
|
||||
onDeleteAssetProfile({
|
||||
dataSource: element.dataSource,
|
||||
symbol: element.symbol
|
||||
})
|
||||
"
|
||||
>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="trash-outline" />
|
||||
<span i18n>Delete</span>
|
||||
</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
||||
<tr
|
||||
*matRowDef="let row; columns: displayedColumns"
|
||||
class="cursor-pointer"
|
||||
mat-row
|
||||
(click)="
|
||||
onOpenAssetProfileDialog({
|
||||
dataSource: row.dataSource,
|
||||
symbol: row.symbol
|
||||
})
|
||||
"
|
||||
></tr>
|
||||
</table>
|
||||
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
||||
<tr
|
||||
*matRowDef="let row; columns: displayedColumns"
|
||||
class="cursor-pointer"
|
||||
mat-row
|
||||
(click)="
|
||||
onOpenAssetProfileDialog({
|
||||
dataSource: row.dataSource,
|
||||
symbol: row.symbol
|
||||
})
|
||||
"
|
||||
></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<mat-paginator
|
||||
[length]="totalItems"
|
||||
@ -222,15 +256,16 @@
|
||||
(page)="onChangePage($event)"
|
||||
/>
|
||||
|
||||
<ngx-skeleton-loader
|
||||
*ngIf="isLoading && totalItems === 0"
|
||||
animation="pulse"
|
||||
class="px-4 py-3"
|
||||
[theme]="{
|
||||
height: '1.5rem',
|
||||
width: '100%'
|
||||
}"
|
||||
/>
|
||||
@if (isLoading && totalItems === 0) {
|
||||
<ngx-skeleton-loader
|
||||
animation="pulse"
|
||||
class="px-4 py-3"
|
||||
[theme]="{
|
||||
height: '1.5rem',
|
||||
width: '100%'
|
||||
}"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -4,6 +4,7 @@ import { GfActivitiesFilterComponent } from '@ghostfolio/ui/activities-filter';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
@ -25,6 +26,7 @@ import { GfCreateAssetProfileDialogModule } from './create-asset-profile-dialog/
|
||||
GfCreateAssetProfileDialogModule,
|
||||
GfSymbolModule,
|
||||
MatButtonModule,
|
||||
MatCheckboxModule,
|
||||
MatMenuModule,
|
||||
MatPaginatorModule,
|
||||
MatSortModule,
|
||||
|
@ -1,14 +1,19 @@
|
||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||
import { ghostfolioScraperApiSymbolPrefix } from '@ghostfolio/common/config';
|
||||
import { getCurrencyFromSymbol, isCurrency } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
AdminMarketDataItem,
|
||||
UniqueAsset
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { takeUntil } from 'rxjs';
|
||||
import { EMPTY, catchError, finalize, forkJoin, takeUntil } from 'rxjs';
|
||||
|
||||
@Injectable()
|
||||
export class AdminMarketDataService {
|
||||
public constructor(private adminService: AdminService) {}
|
||||
|
||||
public deleteProfileData({ dataSource, symbol }: UniqueAsset) {
|
||||
public deleteAssetProfile({ dataSource, symbol }: UniqueAsset) {
|
||||
const confirmation = confirm(
|
||||
$localize`Do you really want to delete this asset profile?`
|
||||
);
|
||||
@ -23,4 +28,44 @@ export class AdminMarketDataService {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public deleteAssetProfiles(uniqueAssets: UniqueAsset[]) {
|
||||
const confirmation = confirm(
|
||||
$localize`Do you really want to delete these profiles?`
|
||||
);
|
||||
|
||||
if (confirmation) {
|
||||
const deleteRequests = uniqueAssets.map(({ dataSource, symbol }) => {
|
||||
return this.adminService.deleteProfileData({ dataSource, symbol });
|
||||
});
|
||||
|
||||
forkJoin(deleteRequests)
|
||||
.pipe(
|
||||
catchError(() => {
|
||||
alert($localize`Oops! Could not delete profiles.`);
|
||||
|
||||
return EMPTY;
|
||||
}),
|
||||
finalize(() => {
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 300);
|
||||
})
|
||||
)
|
||||
.subscribe(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
public hasPermissionToDeleteAssetProfile({
|
||||
activitiesCount,
|
||||
isBenchmark,
|
||||
symbol
|
||||
}: Pick<AdminMarketDataItem, 'activitiesCount' | 'isBenchmark' | 'symbol'>) {
|
||||
return (
|
||||
activitiesCount === 0 &&
|
||||
!isBenchmark &&
|
||||
!isCurrency(getCurrencyFromSymbol(symbol)) &&
|
||||
!symbol.startsWith(ghostfolioScraperApiSymbolPrefix)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -87,7 +87,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor(
|
||||
private adminMarketDataService: AdminMarketDataService,
|
||||
public adminMarketDataService: AdminMarketDataService,
|
||||
private adminService: AdminService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
@Inject(MAT_DIALOG_DATA) public data: AssetProfileDialogParams,
|
||||
@ -176,7 +176,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
public onDeleteProfileData({ dataSource, symbol }: UniqueAsset) {
|
||||
this.adminMarketDataService.deleteProfileData({ dataSource, symbol });
|
||||
this.adminMarketDataService.deleteAssetProfile({ dataSource, symbol });
|
||||
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
@ -48,9 +48,11 @@
|
||||
mat-menu-item
|
||||
type="button"
|
||||
[disabled]="
|
||||
assetProfile?.activitiesCount !== 0 ||
|
||||
isBenchmark ||
|
||||
data.symbol.startsWith(ghostfolioScraperApiSymbolPrefix)
|
||||
!adminMarketDataService.hasPermissionToDeleteAssetProfile({
|
||||
activitiesCount: assetProfile?.activitiesCount,
|
||||
isBenchmark: isBenchmark,
|
||||
symbol: data.symbol
|
||||
})
|
||||
"
|
||||
(click)="
|
||||
onDeleteProfileData({
|
||||
@ -146,7 +148,7 @@
|
||||
i18n
|
||||
size="medium"
|
||||
[locale]="data.locale"
|
||||
[value]="assetProfile?.activitiesCount ?? 0"
|
||||
[value]="assetProfile?.activitiesCount"
|
||||
>Activities</gf-value
|
||||
>
|
||||
</div>
|
||||
@ -243,11 +245,11 @@
|
||||
<mat-label i18n>Asset Class</mat-label>
|
||||
<mat-select formControlName="assetClass">
|
||||
<mat-option [value]="null" />
|
||||
<mat-option
|
||||
*ngFor="let assetClass of assetClasses"
|
||||
[value]="assetClass.id"
|
||||
>{{ assetClass.label }}</mat-option
|
||||
>
|
||||
@for (assetClass of assetClasses; track assetClass) {
|
||||
<mat-option [value]="assetClass.id">{{
|
||||
assetClass.label
|
||||
}}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
@ -256,11 +258,11 @@
|
||||
<mat-label i18n>Asset Sub Class</mat-label>
|
||||
<mat-select formControlName="assetSubClass">
|
||||
<mat-option [value]="null" />
|
||||
<mat-option
|
||||
*ngFor="let assetSubClass of assetSubClasses"
|
||||
[value]="assetSubClass.id"
|
||||
>{{ assetSubClass.label }}</mat-option
|
||||
>
|
||||
@for (assetSubClass of assetSubClasses; track assetSubClass) {
|
||||
<mat-option [value]="assetSubClass.id">{{
|
||||
assetSubClass.label
|
||||
}}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
@ -20,21 +20,24 @@
|
||||
</mat-radio-group>
|
||||
</div>
|
||||
|
||||
<div *ngIf="mode === 'auto'">
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Name, symbol or ISIN</mat-label>
|
||||
<gf-symbol-autocomplete
|
||||
formControlName="searchSymbol"
|
||||
[includeIndices]="true"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div *ngIf="mode === 'manual'">
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Symbol</mat-label>
|
||||
<input formControlName="addSymbol" matInput />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
@if (mode === 'auto') {
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Name, symbol or ISIN</mat-label>
|
||||
<gf-symbol-autocomplete
|
||||
formControlName="searchSymbol"
|
||||
[includeIndices]="true"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
} @else if (mode === 'manual') {
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Symbol</mat-label>
|
||||
<input formControlName="addSymbol" matInput />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="d-flex justify-content-end" mat-dialog-actions>
|
||||
<button i18n mat-button type="button" (click)="onCancel()">Cancel</button>
|
||||
|
@ -27,72 +27,77 @@
|
||||
[precision]="0"
|
||||
[value]="transactionCount"
|
||||
/>
|
||||
<div *ngIf="transactionCount && userCount">
|
||||
{{ transactionCount / userCount | number: '1.2-2' }}
|
||||
<span i18n>per User</span>
|
||||
</div>
|
||||
@if (transactionCount && userCount) {
|
||||
<div>
|
||||
{{ transactionCount / userCount | number: '1.2-2' }}
|
||||
<span i18n>per User</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="align-items-start d-flex my-3">
|
||||
<div class="w-50" i18n>Exchange Rates</div>
|
||||
<div class="w-50">
|
||||
<table>
|
||||
<tr *ngFor="let exchangeRate of exchangeRates">
|
||||
<td>
|
||||
<gf-value [locale]="user?.settings?.locale" [value]="1" />
|
||||
</td>
|
||||
<td class="pl-1">{{ exchangeRate.label1 }}</td>
|
||||
<td class="px-1">=</td>
|
||||
<td align="right">
|
||||
<gf-value
|
||||
class="d-inline-block"
|
||||
[locale]="user?.settings?.locale"
|
||||
[precision]="4"
|
||||
[value]="exchangeRate.value"
|
||||
/>
|
||||
</td>
|
||||
<td class="pl-1">{{ exchangeRate.label2 }}</td>
|
||||
<td>
|
||||
<button
|
||||
class="mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
[matMenuTriggerFor]="exchangeRateActionsMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<ion-icon name="ellipsis-horizontal" />
|
||||
</button>
|
||||
<mat-menu
|
||||
#exchangeRateActionsMenu="matMenu"
|
||||
class="h-100 mx-1 no-min-width px-2"
|
||||
xPosition="before"
|
||||
>
|
||||
<a
|
||||
mat-menu-item
|
||||
[queryParams]="{
|
||||
assetProfileDialog: true,
|
||||
dataSource: exchangeRate.dataSource,
|
||||
symbol: exchangeRate.symbol
|
||||
}"
|
||||
[routerLink]="['/admin', 'market-data']"
|
||||
>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="create-outline" />
|
||||
<span i18n>Edit</span>
|
||||
</span>
|
||||
</a>
|
||||
@for (exchangeRate of exchangeRates; track exchangeRate) {
|
||||
<tr>
|
||||
<td>
|
||||
<gf-value [locale]="user?.settings?.locale" [value]="1" />
|
||||
</td>
|
||||
<td class="pl-1">{{ exchangeRate.label1 }}</td>
|
||||
<td class="px-1">=</td>
|
||||
<td align="right">
|
||||
<gf-value
|
||||
class="d-inline-block"
|
||||
[locale]="user?.settings?.locale"
|
||||
[precision]="4"
|
||||
[value]="exchangeRate.value"
|
||||
/>
|
||||
</td>
|
||||
<td class="pl-1">{{ exchangeRate.label2 }}</td>
|
||||
<td>
|
||||
<button
|
||||
*ngIf="customCurrencies.includes(exchangeRate.label2)"
|
||||
mat-menu-item
|
||||
(click)="onDeleteCurrency(exchangeRate.label2)"
|
||||
class="mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
[matMenuTriggerFor]="exchangeRateActionsMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="trash-outline" />
|
||||
<span i18n>Delete</span>
|
||||
</span>
|
||||
<ion-icon name="ellipsis-horizontal" />
|
||||
</button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
</tr>
|
||||
<mat-menu
|
||||
#exchangeRateActionsMenu="matMenu"
|
||||
class="h-100 mx-1 no-min-width px-2"
|
||||
xPosition="before"
|
||||
>
|
||||
<a
|
||||
mat-menu-item
|
||||
[queryParams]="{
|
||||
assetProfileDialog: true,
|
||||
dataSource: exchangeRate.dataSource,
|
||||
symbol: exchangeRate.symbol
|
||||
}"
|
||||
[routerLink]="['/admin', 'market-data']"
|
||||
>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="create-outline" />
|
||||
<span i18n>Edit</span>
|
||||
</span>
|
||||
</a>
|
||||
@if (customCurrencies.includes(exchangeRate.label2)) {
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="onDeleteCurrency(exchangeRate.label2)"
|
||||
>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="trash-outline" />
|
||||
<span i18n>Delete</span>
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
</mat-menu>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
<div class="mt-2">
|
||||
<button
|
||||
@ -119,17 +124,19 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="hasPermissionToToggleReadOnlyMode" class="d-flex my-3">
|
||||
<div class="w-50" i18n>Read-only Mode</div>
|
||||
<div class="w-50">
|
||||
<mat-slide-toggle
|
||||
color="primary"
|
||||
hideIcon="true"
|
||||
[checked]="info?.isReadOnlyMode"
|
||||
(change)="onReadOnlyModeChange($event)"
|
||||
/>
|
||||
@if (hasPermissionToToggleReadOnlyMode) {
|
||||
<div class="d-flex my-3">
|
||||
<div class="w-50" i18n>Read-only Mode</div>
|
||||
<div class="w-50">
|
||||
<mat-slide-toggle
|
||||
color="primary"
|
||||
hideIcon="true"
|
||||
[checked]="info?.isReadOnlyMode"
|
||||
(change)="onReadOnlyModeChange($event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="d-flex my-3">
|
||||
<div class="w-50" i18n>Data Gathering</div>
|
||||
<div class="w-50">
|
||||
@ -141,99 +148,105 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="hasPermissionForSystemMessage" class="d-flex my-3">
|
||||
<div class="w-50" i18n>System Message</div>
|
||||
<div class="w-50">
|
||||
<div *ngIf="systemMessage" class="align-items-center d-flex">
|
||||
<div class="text-truncate">{{ systemMessage | json }}</div>
|
||||
<button
|
||||
class="h-100 mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
(click)="onDeleteSystemMessage()"
|
||||
>
|
||||
<ion-icon name="trash-outline" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
*ngIf="!info?.systemMessage"
|
||||
class="mt-2"
|
||||
color="accent"
|
||||
mat-flat-button
|
||||
(click)="onSetSystemMessage()"
|
||||
>
|
||||
<ion-icon class="mr-1" name="information-circle-outline" />
|
||||
<span i18n>Set Message</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
class="d-flex my-3 subscription"
|
||||
>
|
||||
<div class="w-50" i18n>Coupons</div>
|
||||
<div class="w-50">
|
||||
<table>
|
||||
<tr *ngFor="let coupon of coupons">
|
||||
<td class="text-monospace">{{ coupon.code }}</td>
|
||||
<td class="pl-2 text-right">{{ coupon.duration }}</td>
|
||||
<td>
|
||||
@if (hasPermissionForSystemMessage) {
|
||||
<div class="d-flex my-3">
|
||||
<div class="w-50" i18n>System Message</div>
|
||||
<div class="w-50">
|
||||
@if (systemMessage) {
|
||||
<div class="align-items-center d-flex">
|
||||
<div class="text-truncate">{{ systemMessage | json }}</div>
|
||||
<button
|
||||
class="mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
[matMenuTriggerFor]="couponActionsMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<ion-icon name="ellipsis-horizontal" />
|
||||
</button>
|
||||
<mat-menu
|
||||
#couponActionsMenu="matMenu"
|
||||
class="h-100 mx-1 no-min-width px-2"
|
||||
xPosition="before"
|
||||
mat-button
|
||||
(click)="onDeleteSystemMessage()"
|
||||
>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="onDeleteCoupon(coupon.code)"
|
||||
>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="trash-outline" />
|
||||
<span i18n>Delete</span>
|
||||
</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="mt-2">
|
||||
<form #couponForm="ngForm" class="align-items-center d-flex">
|
||||
<mat-form-field
|
||||
appearance="outline"
|
||||
class="mr-2 without-hint"
|
||||
>
|
||||
<mat-select
|
||||
name="duration"
|
||||
[value]="couponDuration"
|
||||
(selectionChange)="onChangeCouponDuration($event.value)"
|
||||
>
|
||||
<mat-option value="7 days">7 Days</mat-option>
|
||||
<mat-option value="14 days">14 Days</mat-option>
|
||||
<mat-option value="30 days">30 Days</mat-option>
|
||||
<mat-option value="90 days">90 Days</mat-option>
|
||||
<mat-option value="180 days">180 Days</mat-option>
|
||||
<mat-option value="1 year">1 Year</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<ion-icon name="trash-outline" />
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@if (!info?.systemMessage) {
|
||||
<button
|
||||
class="mt-1"
|
||||
color="primary"
|
||||
class="mt-2"
|
||||
color="accent"
|
||||
mat-flat-button
|
||||
(click)="onAddCoupon()"
|
||||
(click)="onSetSystemMessage()"
|
||||
>
|
||||
<span i18n>Add</span>
|
||||
<ion-icon class="mr-1" name="information-circle-outline" />
|
||||
<span i18n>Set Message</span>
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (hasPermissionForSubscription) {
|
||||
<div class="d-flex my-3 subscription">
|
||||
<div class="w-50" i18n>Coupons</div>
|
||||
<div class="w-50">
|
||||
<table>
|
||||
@for (coupon of coupons; track coupon) {
|
||||
<tr>
|
||||
<td class="text-monospace">{{ coupon.code }}</td>
|
||||
<td class="pl-2 text-right">{{ coupon.duration }}</td>
|
||||
<td>
|
||||
<button
|
||||
class="mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
[matMenuTriggerFor]="couponActionsMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<ion-icon name="ellipsis-horizontal" />
|
||||
</button>
|
||||
<mat-menu
|
||||
#couponActionsMenu="matMenu"
|
||||
class="h-100 mx-1 no-min-width px-2"
|
||||
xPosition="before"
|
||||
>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="onDeleteCoupon(coupon.code)"
|
||||
>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="trash-outline" />
|
||||
<span i18n>Delete</span>
|
||||
</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
<div class="mt-2">
|
||||
<form #couponForm="ngForm" class="align-items-center d-flex">
|
||||
<mat-form-field
|
||||
appearance="outline"
|
||||
class="mr-2 without-hint"
|
||||
>
|
||||
<mat-select
|
||||
name="duration"
|
||||
[value]="couponDuration"
|
||||
(selectionChange)="onChangeCouponDuration($event.value)"
|
||||
>
|
||||
<mat-option value="7 days">7 Days</mat-option>
|
||||
<mat-option value="14 days">14 Days</mat-option>
|
||||
<mat-option value="30 days">30 Days</mat-option>
|
||||
<mat-option value="90 days">90 Days</mat-option>
|
||||
<mat-option value="180 days">180 Days</mat-option>
|
||||
<mat-option value="1 year">1 Year</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<button
|
||||
class="mt-1"
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
(click)="onAddCoupon()"
|
||||
>
|
||||
<span i18n>Add</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="d-flex my-3">
|
||||
<div class="w-50" i18n>Housekeeping</div>
|
||||
<div class="w-50">
|
||||
|
@ -30,12 +30,13 @@
|
||||
<ng-container i18n>Name</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
<gf-asset-profile-icon
|
||||
*ngIf="element.url"
|
||||
class="d-inline mr-1"
|
||||
[tooltip]="element.name"
|
||||
[url]="element.url"
|
||||
/>
|
||||
@if (element.url) {
|
||||
<gf-asset-profile-icon
|
||||
class="d-inline mr-1"
|
||||
[tooltip]="element.name"
|
||||
[url]="element.url"
|
||||
/>
|
||||
}
|
||||
<span>{{ element.name }}</span>
|
||||
</td></ng-container
|
||||
>
|
||||
|
@ -4,8 +4,11 @@
|
||||
(keyup.enter)="platformForm.valid && onSubmit()"
|
||||
(ngSubmit)="onSubmit()"
|
||||
>
|
||||
<h1 *ngIf="data.platform.id" i18n mat-dialog-title>Update platform</h1>
|
||||
<h1 *ngIf="!data.platform.id" i18n mat-dialog-title>Add platform</h1>
|
||||
@if (data.platform.id) {
|
||||
<h1 i18n mat-dialog-title>Update platform</h1>
|
||||
} @else {
|
||||
<h1 i18n mat-dialog-title>Add platform</h1>
|
||||
}
|
||||
<div class="flex-grow-1 py-3" mat-dialog-content>
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
|
@ -4,8 +4,11 @@
|
||||
(keyup.enter)="tagForm.valid && onSubmit()"
|
||||
(ngSubmit)="onSubmit()"
|
||||
>
|
||||
<h1 *ngIf="data.tag.id" i18n mat-dialog-title>Update tag</h1>
|
||||
<h1 *ngIf="!data.tag.id" i18n mat-dialog-title>Add tag</h1>
|
||||
@if (data.tag.id) {
|
||||
<h1 i18n mat-dialog-title>Update tag</h1>
|
||||
} @else {
|
||||
<h1 i18n mat-dialog-title>Add tag</h1>
|
||||
}
|
||||
<div class="flex-grow-1 py-3" mat-dialog-content>
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="users">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="gf-table" mat-table [dataSource]="dataSource">
|
||||
<ng-container matColumnDef="index">
|
||||
<th
|
||||
@ -49,43 +49,44 @@
|
||||
}"
|
||||
>{{ (element.id | slice: 0 : 5) + '...' }}</span
|
||||
>
|
||||
<gf-premium-indicator
|
||||
*ngIf="element?.subscription?.type === 'Premium'"
|
||||
class="ml-1"
|
||||
[enableLink]="false"
|
||||
[title]="
|
||||
'Expires ' +
|
||||
formatDistanceToNow(element.subscription.expiresAt) +
|
||||
' (' +
|
||||
(element.subscription.expiresAt | date: defaultDateFormat) +
|
||||
')'
|
||||
"
|
||||
/>
|
||||
@if (element?.subscription?.type === 'Premium') {
|
||||
<gf-premium-indicator
|
||||
class="ml-1"
|
||||
[enableLink]="false"
|
||||
[title]="
|
||||
'Expires ' +
|
||||
formatDistanceToNow(element.subscription.expiresAt) +
|
||||
' (' +
|
||||
(element.subscription.expiresAt
|
||||
| date: defaultDateFormat) +
|
||||
')'
|
||||
"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
matColumnDef="country"
|
||||
>
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="mat-mdc-header-cell px-1 py-2"
|
||||
mat-header-cell
|
||||
>
|
||||
<ng-container i18n>Country</ng-container>
|
||||
</th>
|
||||
<td
|
||||
*matCellDef="let element"
|
||||
class="mat-mdc-cell px-1 py-2"
|
||||
mat-cell
|
||||
>
|
||||
<span class="h5" [title]="element.country">{{
|
||||
getEmojiFlag(element.country)
|
||||
}}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
@if (hasPermissionForSubscription) {
|
||||
<ng-container matColumnDef="country">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="mat-mdc-header-cell px-1 py-2"
|
||||
mat-header-cell
|
||||
>
|
||||
<ng-container i18n>Country</ng-container>
|
||||
</th>
|
||||
<td
|
||||
*matCellDef="let element"
|
||||
class="mat-mdc-cell px-1 py-2"
|
||||
mat-cell
|
||||
>
|
||||
<span class="h5" [title]="element.country">{{
|
||||
getEmojiFlag(element.country)
|
||||
}}</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
<ng-container matColumnDef="registration">
|
||||
<th
|
||||
@ -146,51 +147,49 @@
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
matColumnDef="engagementPerDay"
|
||||
>
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="mat-mdc-header-cell px-1 py-2 text-right"
|
||||
mat-header-cell
|
||||
>
|
||||
<ng-container i18n>Engagement per Day</ng-container>
|
||||
</th>
|
||||
<td
|
||||
*matCellDef="let element"
|
||||
class="mat-mdc-cell px-1 py-2 text-right"
|
||||
mat-cell
|
||||
>
|
||||
<gf-value
|
||||
class="d-inline-block justify-content-end"
|
||||
[locale]="user?.settings?.locale"
|
||||
[precision]="0"
|
||||
[value]="element.engagement"
|
||||
/>
|
||||
</td>
|
||||
</ng-container>
|
||||
@if (hasPermissionForSubscription) {
|
||||
<ng-container matColumnDef="engagementPerDay">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="mat-mdc-header-cell px-1 py-2 text-right"
|
||||
mat-header-cell
|
||||
>
|
||||
<ng-container i18n>Engagement per Day</ng-container>
|
||||
</th>
|
||||
<td
|
||||
*matCellDef="let element"
|
||||
class="mat-mdc-cell px-1 py-2 text-right"
|
||||
mat-cell
|
||||
>
|
||||
<gf-value
|
||||
class="d-inline-block justify-content-end"
|
||||
[locale]="user?.settings?.locale"
|
||||
[precision]="0"
|
||||
[value]="element.engagement"
|
||||
/>
|
||||
</td>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
<ng-container
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
matColumnDef="lastRequest"
|
||||
>
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="mat-mdc-header-cell px-1 py-2"
|
||||
i18n
|
||||
mat-header-cell
|
||||
>
|
||||
Last Request
|
||||
</th>
|
||||
<td
|
||||
*matCellDef="let element"
|
||||
class="mat-mdc-cell px-1 py-2"
|
||||
mat-cell
|
||||
>
|
||||
{{ formatDistanceToNow(element.lastActivity) }}
|
||||
</td>
|
||||
</ng-container>
|
||||
@if (hasPermissionForSubscription) {
|
||||
<ng-container matColumnDef="lastRequest">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="mat-mdc-header-cell px-1 py-2"
|
||||
i18n
|
||||
mat-header-cell
|
||||
>
|
||||
Last Request
|
||||
</th>
|
||||
<td
|
||||
*matCellDef="let element"
|
||||
class="mat-mdc-cell px-1 py-2"
|
||||
mat-cell
|
||||
>
|
||||
{{ formatDistanceToNow(element.lastActivity) }}
|
||||
</td>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
<ng-container matColumnDef="actions" stickyEnd>
|
||||
<th
|
||||
@ -212,16 +211,14 @@
|
||||
<ion-icon name="ellipsis-horizontal" />
|
||||
</button>
|
||||
<mat-menu #userMenu="matMenu" xPosition="before">
|
||||
<button
|
||||
*ngIf="hasPermissionToImpersonateAllUsers"
|
||||
mat-menu-item
|
||||
(click)="onImpersonateUser(element.id)"
|
||||
>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="contract-outline" />
|
||||
<span i18n>Impersonate User</span>
|
||||
</span>
|
||||
</button>
|
||||
@if (hasPermissionToImpersonateAllUsers) {
|
||||
<button mat-menu-item (click)="onImpersonateUser(element.id)">
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2" name="contract-outline" />
|
||||
<span i18n>Impersonate User</span>
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
mat-menu-item
|
||||
[disabled]="element.id === user?.id"
|
||||
|
@ -1,16 +1,12 @@
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
.users {
|
||||
overflow-x: auto;
|
||||
.gf-table {
|
||||
min-width: 100%;
|
||||
|
||||
table {
|
||||
min-width: 100%;
|
||||
|
||||
.mat-mdc-row,
|
||||
.mat-mdc-header-row {
|
||||
width: 100%;
|
||||
}
|
||||
.mat-mdc-row,
|
||||
.mat-mdc-header-row {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,10 +4,9 @@
|
||||
class="align-items-center d-flex flex-grow-1 h5 mb-0 py-2 text-truncate"
|
||||
>
|
||||
<span i18n>Performance</span>
|
||||
<gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
/>
|
||||
@if (user?.subscription?.type === 'Basic') {
|
||||
<gf-premium-indicator class="ml-1" />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-xs-12 d-flex justify-content-end">
|
||||
@ -24,33 +23,33 @@
|
||||
(selectionChange)="onChangeBenchmark($event.value)"
|
||||
>
|
||||
<mat-option [value]="null" />
|
||||
<mat-option
|
||||
*ngFor="let symbolProfile of benchmarks"
|
||||
[value]="symbolProfile.id"
|
||||
>{{ symbolProfile.name }}</mat-option
|
||||
>
|
||||
<mat-option
|
||||
*ngIf="hasPermissionToAccessAdminControl"
|
||||
[routerLink]="['/admin', 'market-data']"
|
||||
>
|
||||
<div class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2 text-muted" name="arrow-forward-outline" />
|
||||
<span i18n>Manage Benchmarks</span>
|
||||
</div>
|
||||
</mat-option>
|
||||
@for (symbolProfile of benchmarks; track symbolProfile) {
|
||||
<mat-option [value]="symbolProfile.id">{{
|
||||
symbolProfile.name
|
||||
}}</mat-option>
|
||||
}
|
||||
@if (hasPermissionToAccessAdminControl) {
|
||||
<mat-option [routerLink]="['/admin', 'market-data']">
|
||||
<div class="align-items-center d-flex">
|
||||
<ion-icon class="mr-2 text-muted" name="arrow-forward-outline" />
|
||||
<span i18n>Manage Benchmarks</span>
|
||||
</div>
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<ngx-skeleton-loader
|
||||
*ngIf="isLoading"
|
||||
animation="pulse"
|
||||
[theme]="{
|
||||
height: '100%',
|
||||
width: '100%'
|
||||
}"
|
||||
/>
|
||||
@if (isLoading) {
|
||||
<ngx-skeleton-loader
|
||||
animation="pulse"
|
||||
[theme]="{
|
||||
height: '100%',
|
||||
width: '100%'
|
||||
}"
|
||||
/>
|
||||
}
|
||||
<canvas
|
||||
#chartCanvas
|
||||
class="h-100"
|
||||
|
@ -1,7 +1,5 @@
|
||||
<button
|
||||
*ngIf="deviceType === 'mobile'"
|
||||
mat-button
|
||||
(click)="onClickCloseButton()"
|
||||
>
|
||||
<ion-icon name="close" size="large" />
|
||||
</button>
|
||||
@if (deviceType === 'mobile') {
|
||||
<button mat-button (click)="onClickCloseButton()">
|
||||
<ion-icon name="close" size="large" />
|
||||
</button>
|
||||
}
|
||||
|
@ -3,11 +3,8 @@
|
||||
[ngClass]="{ 'text-center': position === 'center' }"
|
||||
>{{ title }}</span
|
||||
>
|
||||
<button
|
||||
*ngIf="deviceType !== 'mobile'"
|
||||
class="no-min-width px-0"
|
||||
mat-button
|
||||
(click)="onClickCloseButton()"
|
||||
>
|
||||
<ion-icon name="close" size="large" />
|
||||
</button>
|
||||
@if (deviceType !== 'mobile') {
|
||||
<button class="no-min-width px-0" mat-button (click)="onClickCloseButton()">
|
||||
<ion-icon name="close" size="large" />
|
||||
</button>
|
||||
}
|
||||
|
@ -12,12 +12,13 @@
|
||||
<small class="d-block" i18n>Current Market Mood</small>
|
||||
</div>
|
||||
</div>
|
||||
<ngx-skeleton-loader
|
||||
*ngIf="!fearAndGreedIndex"
|
||||
animation="pulse"
|
||||
class="position-absolute w-100"
|
||||
[theme]="{
|
||||
height: '100%'
|
||||
}"
|
||||
/>
|
||||
@if (!fearAndGreedIndex) {
|
||||
<ngx-skeleton-loader
|
||||
animation="pulse"
|
||||
class="position-absolute w-100"
|
||||
[theme]="{
|
||||
height: '100%'
|
||||
}"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<mat-toolbar class="px-0">
|
||||
<ng-container *ngIf="user">
|
||||
@if (user) {
|
||||
<div class="d-flex h-100 logo-container" [ngClass]="{ filled: hasTabs }">
|
||||
<a
|
||||
class="align-items-center justify-content-start rounded-0"
|
||||
@ -54,19 +54,21 @@
|
||||
>Accounts</a
|
||||
>
|
||||
</li>
|
||||
<li *ngIf="hasPermissionToAccessAdminControl" class="list-inline-item">
|
||||
<a
|
||||
class="d-none d-sm-block"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'admin',
|
||||
'text-decoration-underline': currentRoute === 'admin'
|
||||
}"
|
||||
[routerLink]="['/admin']"
|
||||
>Admin Control</a
|
||||
>
|
||||
</li>
|
||||
@if (hasPermissionToAccessAdminControl) {
|
||||
<li class="list-inline-item">
|
||||
<a
|
||||
class="d-none d-sm-block"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'admin',
|
||||
'text-decoration-underline': currentRoute === 'admin'
|
||||
}"
|
||||
[routerLink]="['/admin']"
|
||||
>Admin Control</a
|
||||
>
|
||||
</li>
|
||||
}
|
||||
<li class="list-inline-item">
|
||||
<a
|
||||
class="d-none d-sm-block"
|
||||
@ -80,24 +82,23 @@
|
||||
>Resources</a
|
||||
>
|
||||
</li>
|
||||
<li
|
||||
*ngIf="
|
||||
hasPermissionForSubscription && user?.subscription?.type === 'Basic'
|
||||
"
|
||||
class="list-inline-item"
|
||||
>
|
||||
<a
|
||||
class="d-none d-sm-block"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === routePricing,
|
||||
'text-decoration-underline': currentRoute === routePricing
|
||||
}"
|
||||
[routerLink]="routerLinkPricing"
|
||||
>Pricing</a
|
||||
>
|
||||
</li>
|
||||
@if (
|
||||
hasPermissionForSubscription && user?.subscription?.type === 'Basic'
|
||||
) {
|
||||
<li class="list-inline-item">
|
||||
<a
|
||||
class="d-none d-sm-block"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === routePricing,
|
||||
'text-decoration-underline': currentRoute === routePricing
|
||||
}"
|
||||
[routerLink]="routerLinkPricing"
|
||||
>Pricing</a
|
||||
>
|
||||
</li>
|
||||
}
|
||||
<li class="list-inline-item">
|
||||
<a
|
||||
class="d-none d-sm-block"
|
||||
@ -111,42 +112,42 @@
|
||||
>About</a
|
||||
>
|
||||
</li>
|
||||
<li *ngIf="hasPermissionToAccessAssistant" class="list-inline-item">
|
||||
<button
|
||||
#assistantTrigger="matMenuTrigger"
|
||||
class="h-100 no-min-width px-2"
|
||||
mat-button
|
||||
matBadge="✓"
|
||||
matBadgeSize="small"
|
||||
[mat-menu-trigger-for]="assistantMenu"
|
||||
[matBadgeHidden]="
|
||||
!hasFilters || !user?.settings?.isExperimentalFeatures
|
||||
"
|
||||
[matMenuTriggerRestoreFocus]="false"
|
||||
(menuOpened)="onOpenAssistant()"
|
||||
>
|
||||
<ion-icon class="rotate-90" name="options-outline" />
|
||||
</button>
|
||||
<mat-menu
|
||||
#assistantMenu="matMenu"
|
||||
class="assistant"
|
||||
xPosition="before"
|
||||
[overlapTrigger]="true"
|
||||
(closed)="assistantElement?.setIsOpen(false)"
|
||||
>
|
||||
<gf-assistant
|
||||
#assistant
|
||||
[deviceType]="deviceType"
|
||||
[hasPermissionToAccessAdminControl]="
|
||||
hasPermissionToAccessAdminControl
|
||||
"
|
||||
[user]="user"
|
||||
(closed)="closeAssistant()"
|
||||
(dateRangeChanged)="onDateRangeChange($event)"
|
||||
(filtersChanged)="onFiltersChanged($event)"
|
||||
/>
|
||||
</mat-menu>
|
||||
</li>
|
||||
@if (hasPermissionToAccessAssistant) {
|
||||
<li class="list-inline-item">
|
||||
<button
|
||||
#assistantTrigger="matMenuTrigger"
|
||||
class="h-100 no-min-width px-2"
|
||||
mat-button
|
||||
matBadge="✓"
|
||||
matBadgeSize="small"
|
||||
[mat-menu-trigger-for]="assistantMenu"
|
||||
[matBadgeHidden]="!hasFilters"
|
||||
[matMenuTriggerRestoreFocus]="false"
|
||||
(menuOpened)="onOpenAssistant()"
|
||||
>
|
||||
<ion-icon class="rotate-90" name="options-outline" />
|
||||
</button>
|
||||
<mat-menu
|
||||
#assistantMenu="matMenu"
|
||||
class="assistant"
|
||||
xPosition="before"
|
||||
[overlapTrigger]="true"
|
||||
(closed)="assistantElement?.setIsOpen(false)"
|
||||
>
|
||||
<gf-assistant
|
||||
#assistant
|
||||
[deviceType]="deviceType"
|
||||
[hasPermissionToAccessAdminControl]="
|
||||
hasPermissionToAccessAdminControl
|
||||
"
|
||||
[user]="user"
|
||||
(closed)="closeAssistant()"
|
||||
(dateRangeChanged)="onDateRangeChange($event)"
|
||||
(filtersChanged)="onFiltersChanged($event)"
|
||||
/>
|
||||
</mat-menu>
|
||||
</li>
|
||||
}
|
||||
<li class="list-inline-item">
|
||||
<button
|
||||
class="no-min-width px-1"
|
||||
@ -167,40 +168,31 @@
|
||||
/>
|
||||
</button>
|
||||
<mat-menu #accountMenu="matMenu" xPosition="before">
|
||||
<ng-container
|
||||
*ngIf="
|
||||
hasPermissionForSubscription &&
|
||||
user?.subscription?.type === 'Basic'
|
||||
"
|
||||
>
|
||||
@if (
|
||||
hasPermissionForSubscription && user?.subscription?.type === 'Basic'
|
||||
) {
|
||||
<a class="d-flex" mat-menu-item [routerLink]="routerLinkPricing"
|
||||
><span class="align-items-center d-flex"
|
||||
><span
|
||||
><ng-container
|
||||
*ngIf="user.subscription.offer === 'default'"
|
||||
i18n
|
||||
>Upgrade Plan</ng-container
|
||||
>
|
||||
<ng-container
|
||||
*ngIf="
|
||||
user.subscription.offer === 'renewal' ||
|
||||
user.subscription.offer === 'renewal-early-bird'
|
||||
"
|
||||
i18n
|
||||
>Renew Plan</ng-container
|
||||
></span
|
||||
>
|
||||
><span>
|
||||
@if (user.subscription.offer === 'default') {
|
||||
<ng-container i18n>Upgrade Plan</ng-container>
|
||||
} @else if (
|
||||
user.subscription.offer === 'renewal' ||
|
||||
user.subscription.offer === 'renewal-early-bird'
|
||||
) {
|
||||
<ng-container i18n>Renew Plan</ng-container>
|
||||
}
|
||||
</span>
|
||||
<gf-premium-indicator
|
||||
class="d-inline-block ml-1"
|
||||
[enableLink]="false" /></span
|
||||
></a>
|
||||
<hr class="m-0" />
|
||||
</ng-container>
|
||||
<ng-container *ngIf="user?.access?.length > 0">
|
||||
}
|
||||
@if (user?.access?.length > 0) {
|
||||
<button mat-menu-item (click)="impersonateAccount(null)">
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon
|
||||
*ngIf="user?.access?.length > 0"
|
||||
class="mr-2"
|
||||
[name]="
|
||||
impersonationId
|
||||
@ -211,27 +203,28 @@
|
||||
<span i18n>Me</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
*ngFor="let accessItem of user?.access"
|
||||
mat-menu-item
|
||||
(click)="impersonateAccount(accessItem.id)"
|
||||
>
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon
|
||||
class="mr-2"
|
||||
name="square-outline"
|
||||
[name]="
|
||||
accessItem.id === impersonationId
|
||||
? 'radio-button-on-outline'
|
||||
: 'radio-button-off-outline'
|
||||
"
|
||||
/>
|
||||
<span *ngIf="accessItem.alias">{{ accessItem.alias }}</span>
|
||||
<span *ngIf="!accessItem.alias" i18n>User</span>
|
||||
</span>
|
||||
</button>
|
||||
@for (accessItem of user?.access; track accessItem) {
|
||||
<button mat-menu-item (click)="impersonateAccount(accessItem.id)">
|
||||
<span class="align-items-center d-flex">
|
||||
<ion-icon
|
||||
class="mr-2"
|
||||
name="square-outline"
|
||||
[name]="
|
||||
accessItem.id === impersonationId
|
||||
? 'radio-button-on-outline'
|
||||
: 'radio-button-off-outline'
|
||||
"
|
||||
/>
|
||||
@if (accessItem.alias) {
|
||||
<span>{{ accessItem.alias }}</span>
|
||||
} @else {
|
||||
<span i18n>User</span>
|
||||
}
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
<hr class="m-0" />
|
||||
</ng-container>
|
||||
}
|
||||
<a
|
||||
class="d-flex d-sm-none"
|
||||
i18n
|
||||
@ -268,15 +261,16 @@
|
||||
[routerLink]="['/account']"
|
||||
>My Ghostfolio</a
|
||||
>
|
||||
<a
|
||||
*ngIf="hasPermissionToAccessAdminControl"
|
||||
class="d-flex d-sm-none"
|
||||
i18n
|
||||
mat-menu-item
|
||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'admin' }"
|
||||
[routerLink]="['/admin']"
|
||||
>Admin Control</a
|
||||
>
|
||||
@if (hasPermissionToAccessAdminControl) {
|
||||
<a
|
||||
class="d-flex d-sm-none"
|
||||
i18n
|
||||
mat-menu-item
|
||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'admin' }"
|
||||
[routerLink]="['/admin']"
|
||||
>Admin Control</a
|
||||
>
|
||||
}
|
||||
<hr class="m-0" />
|
||||
<a
|
||||
class="d-flex d-sm-none"
|
||||
@ -288,18 +282,18 @@
|
||||
[routerLink]="routerLinkResources"
|
||||
>Resources</a
|
||||
>
|
||||
<a
|
||||
*ngIf="
|
||||
hasPermissionForSubscription &&
|
||||
user?.subscription?.type === 'Basic'
|
||||
"
|
||||
class="d-flex d-sm-none"
|
||||
i18n
|
||||
mat-menu-item
|
||||
[ngClass]="{ 'font-weight-bold': currentRoute === routePricing }"
|
||||
[routerLink]="routerLinkPricing"
|
||||
>Pricing</a
|
||||
>
|
||||
@if (
|
||||
hasPermissionForSubscription && user?.subscription?.type === 'Basic'
|
||||
) {
|
||||
<a
|
||||
class="d-flex d-sm-none"
|
||||
i18n
|
||||
mat-menu-item
|
||||
[ngClass]="{ 'font-weight-bold': currentRoute === routePricing }"
|
||||
[routerLink]="routerLinkPricing"
|
||||
>Pricing</a
|
||||
>
|
||||
}
|
||||
<a
|
||||
class="d-flex d-sm-none"
|
||||
i18n
|
||||
@ -313,8 +307,8 @@
|
||||
</mat-menu>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="user === null">
|
||||
}
|
||||
@if (user === null) {
|
||||
<div class="d-flex h-100 logo-container" [ngClass]="{ filled: hasTabs }">
|
||||
<a
|
||||
class="align-items-center justify-content-start rounded-0"
|
||||
@ -357,35 +351,36 @@
|
||||
>About</a
|
||||
>
|
||||
</li>
|
||||
<li *ngIf="hasPermissionForSubscription" class="list-inline-item">
|
||||
<a
|
||||
class="d-sm-block"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === routePricing,
|
||||
'text-decoration-underline': currentRoute === routePricing
|
||||
}"
|
||||
[routerLink]="routerLinkPricing"
|
||||
>Pricing</a
|
||||
>
|
||||
</li>
|
||||
<li
|
||||
*ngIf="hasPermissionToAccessFearAndGreedIndex"
|
||||
class="list-inline-item"
|
||||
>
|
||||
<a
|
||||
class="d-none d-sm-block"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === routeMarkets,
|
||||
'text-decoration-underline': currentRoute === routeMarkets
|
||||
}"
|
||||
[routerLink]="routerLinkMarkets"
|
||||
>Markets</a
|
||||
>
|
||||
</li>
|
||||
@if (hasPermissionForSubscription) {
|
||||
<li class="list-inline-item">
|
||||
<a
|
||||
class="d-sm-block"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === routePricing,
|
||||
'text-decoration-underline': currentRoute === routePricing
|
||||
}"
|
||||
[routerLink]="routerLinkPricing"
|
||||
>Pricing</a
|
||||
>
|
||||
</li>
|
||||
}
|
||||
@if (hasPermissionToAccessFearAndGreedIndex) {
|
||||
<li class="list-inline-item">
|
||||
<a
|
||||
class="d-none d-sm-block"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === routeMarkets,
|
||||
'text-decoration-underline': currentRoute === routeMarkets
|
||||
}"
|
||||
[routerLink]="routerLinkMarkets"
|
||||
>Markets</a
|
||||
>
|
||||
</li>
|
||||
}
|
||||
<li class="list-inline-item">
|
||||
<a
|
||||
class="d-none d-sm-block no-min-width p-1"
|
||||
@ -399,18 +394,17 @@
|
||||
<ng-container i18n>Sign in</ng-container>
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
*ngIf="currentRoute !== 'register' && hasPermissionToCreateUser"
|
||||
class="list-inline-item ml-1"
|
||||
>
|
||||
<a
|
||||
class="d-none d-sm-block"
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
[routerLink]="routerLinkRegister"
|
||||
><ng-container i18n>Get started</ng-container>
|
||||
</a>
|
||||
</li>
|
||||
@if (currentRoute !== 'register' && hasPermissionToCreateUser) {
|
||||
<li class="list-inline-item ml-1">
|
||||
<a
|
||||
class="d-none d-sm-block"
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
[routerLink]="routerLinkRegister"
|
||||
><ng-container i18n>Get started</ng-container>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</ng-container>
|
||||
}
|
||||
</mat-toolbar>
|
||||
|
@ -376,9 +376,9 @@
|
||||
<div class="col">
|
||||
<div class="h5" i18n>Tags</div>
|
||||
<mat-chip-listbox>
|
||||
<mat-chip-option *ngFor="let tag of tags" disabled>{{
|
||||
tag.name
|
||||
}}</mat-chip-option>
|
||||
@for (tag of tags; track tag) {
|
||||
<mat-chip-option disabled>{{ tag.name }}</mat-chip-option>
|
||||
}
|
||||
</mat-chip-listbox>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@ -21,6 +22,7 @@ import { takeUntil } from 'rxjs/operators';
|
||||
})
|
||||
export class HomeMarketComponent implements OnDestroy, OnInit {
|
||||
public benchmarks: Benchmark[];
|
||||
public deviceType: string;
|
||||
public fearAndGreedIndex: number;
|
||||
public fearLabel = $localize`Fear`;
|
||||
public greedLabel = $localize`Greed`;
|
||||
@ -36,8 +38,10 @@ export class HomeMarketComponent implements OnDestroy, OnInit {
|
||||
public constructor(
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
private deviceService: DeviceDetectorService,
|
||||
private userService: UserService
|
||||
) {
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
this.info = this.dataService.fetchInfo();
|
||||
this.isLoading = true;
|
||||
|
||||
|
@ -1,47 +1,51 @@
|
||||
<div class="container">
|
||||
<h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>Markets</h1>
|
||||
<div *ngIf="hasPermissionToAccessFearAndGreedIndex" class="mb-5 row">
|
||||
<div class="col-xs-12 col-md-8 offset-md-2">
|
||||
<div class="mb-2 text-center text-muted">
|
||||
<small i18n>Last {{ numberOfDays }} Days</small>
|
||||
@if (hasPermissionToAccessFearAndGreedIndex) {
|
||||
<div class="mb-5 row">
|
||||
<div class="col-xs-12 col-md-8 offset-md-2">
|
||||
<div class="mb-2 text-center text-muted">
|
||||
<small i18n>Last {{ numberOfDays }} Days</small>
|
||||
</div>
|
||||
<gf-line-chart
|
||||
class="mb-3"
|
||||
symbol="Fear & Greed Index"
|
||||
[colorScheme]="user?.settings?.colorScheme"
|
||||
[historicalDataItems]="historicalDataItems"
|
||||
[isAnimated]="true"
|
||||
[locale]="user?.settings?.locale || undefined"
|
||||
[showXAxis]="true"
|
||||
[showYAxis]="true"
|
||||
[yMax]="100"
|
||||
[yMaxLabel]="greedLabel"
|
||||
[yMin]="0"
|
||||
[yMinLabel]="fearLabel"
|
||||
/>
|
||||
<gf-fear-and-greed-index
|
||||
class="d-flex justify-content-center"
|
||||
[fearAndGreedIndex]="fearAndGreedIndex"
|
||||
/>
|
||||
</div>
|
||||
<gf-line-chart
|
||||
class="mb-3"
|
||||
symbol="Fear & Greed Index"
|
||||
[colorScheme]="user?.settings?.colorScheme"
|
||||
[historicalDataItems]="historicalDataItems"
|
||||
[isAnimated]="true"
|
||||
[locale]="user?.settings?.locale || undefined"
|
||||
[showXAxis]="true"
|
||||
[showYAxis]="true"
|
||||
[yMax]="100"
|
||||
[yMaxLabel]="greedLabel"
|
||||
[yMin]="0"
|
||||
[yMinLabel]="fearLabel"
|
||||
/>
|
||||
<gf-fear-and-greed-index
|
||||
class="d-flex justify-content-center"
|
||||
[fearAndGreedIndex]="fearAndGreedIndex"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="mb-3 row">
|
||||
<div class="col-xs-12 col-md-8 offset-md-2">
|
||||
<gf-benchmark
|
||||
[benchmarks]="benchmarks"
|
||||
[deviceType]="deviceType"
|
||||
[locale]="user?.settings?.locale || undefined"
|
||||
[user]="user"
|
||||
/>
|
||||
<ngx-skeleton-loader
|
||||
*ngIf="isLoading"
|
||||
animation="pulse"
|
||||
class="px-2 py-3"
|
||||
[theme]="{
|
||||
height: '1.5rem',
|
||||
width: '100%'
|
||||
}"
|
||||
/>
|
||||
@if (isLoading) {
|
||||
<ngx-skeleton-loader
|
||||
animation="pulse"
|
||||
class="px-2 py-3"
|
||||
[theme]="{
|
||||
height: '1.5rem',
|
||||
width: '100%'
|
||||
}"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -39,22 +39,19 @@
|
||||
</li>
|
||||
</ol>
|
||||
<div class="d-flex justify-content-center">
|
||||
<a
|
||||
*ngIf="user?.accounts?.length === 1"
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
[routerLink]="['/accounts']"
|
||||
>
|
||||
<ng-container i18n>Setup accounts</ng-container>
|
||||
</a>
|
||||
<a
|
||||
*ngIf="user?.accounts?.length > 1"
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
[routerLink]="['/portfolio', 'activities']"
|
||||
>
|
||||
<ng-container i18n>Add activity</ng-container>
|
||||
</a>
|
||||
@if (user?.accounts?.length === 1) {
|
||||
<a color="primary" mat-flat-button [routerLink]="['/accounts']">
|
||||
<ng-container i18n>Setup accounts</ng-container>
|
||||
</a>
|
||||
} @else if (user?.accounts?.length > 1) {
|
||||
<a
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
[routerLink]="['/portfolio', 'activities']"
|
||||
>
|
||||
<ng-container i18n>Add activity</ng-container>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -9,12 +9,7 @@ import {
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import {
|
||||
MatSnackBar,
|
||||
MatSnackBarRef,
|
||||
TextOnlySnackBar
|
||||
} from '@angular/material/snack-bar';
|
||||
import { Router } from '@angular/router';
|
||||
import { MatSnackBarRef, TextOnlySnackBar } from '@angular/material/snack-bar';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@ -39,8 +34,6 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
private impersonationStorageService: ImpersonationStorageService,
|
||||
private router: Router,
|
||||
private snackBar: MatSnackBar,
|
||||
private userService: UserService
|
||||
) {
|
||||
this.info = this.dataService.fetchInfo();
|
||||
@ -108,24 +101,6 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
|
||||
this.summary = summary;
|
||||
this.isLoading = false;
|
||||
|
||||
if (!this.summary) {
|
||||
this.snackBarRef = this.snackBar.open(
|
||||
$localize`This feature requires a subscription.`,
|
||||
this.hasPermissionForSubscription
|
||||
? $localize`Upgrade Plan`
|
||||
: undefined,
|
||||
{ duration: 6000 }
|
||||
);
|
||||
|
||||
this.snackBarRef.afterDismissed().subscribe(() => {
|
||||
this.snackBarRef = undefined;
|
||||
});
|
||||
|
||||
this.snackBarRef.onAction().subscribe(() => {
|
||||
this.router.navigate(['/' + $localize`pricing`]);
|
||||
});
|
||||
}
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
|
@ -13,6 +13,7 @@
|
||||
[language]="user?.settings?.language"
|
||||
[locale]="user?.settings?.locale"
|
||||
[summary]="summary"
|
||||
[user]="user"
|
||||
(emergencyFundChanged)="onChangeEmergencyFund($event)"
|
||||
/>
|
||||
</mat-card-content>
|
||||
|
@ -3,18 +3,12 @@ import { GfPortfolioSummaryModule } from '@ghostfolio/client/components/portfoli
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { HomeSummaryComponent } from './home-summary.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [HomeSummaryComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfPortfolioSummaryModule,
|
||||
MatCardModule,
|
||||
RouterModule
|
||||
],
|
||||
imports: [CommonModule, GfPortfolioSummaryModule, MatCardModule],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfHomeSummaryModule {}
|
||||
|
@ -1,11 +1,12 @@
|
||||
<ngx-skeleton-loader
|
||||
*ngIf="isLoading"
|
||||
animation="pulse"
|
||||
[theme]="{
|
||||
height: '100%',
|
||||
width: '100%'
|
||||
}"
|
||||
/>
|
||||
@if (isLoading) {
|
||||
<ngx-skeleton-loader
|
||||
animation="pulse"
|
||||
[theme]="{
|
||||
height: '100%',
|
||||
width: '100%'
|
||||
}"
|
||||
/>
|
||||
}
|
||||
<canvas
|
||||
#chartCanvas
|
||||
class="h-100"
|
||||
|
@ -27,7 +27,7 @@
|
||||
</button>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
<ng-container *ngIf="data.hasPermissionToUseSocialLogin">
|
||||
@if (data.hasPermissionToUseSocialLogin) {
|
||||
<div class="my-3 text-center text-muted" i18n>or</div>
|
||||
<div class="d-flex flex-column">
|
||||
<button
|
||||
@ -52,7 +52,7 @@
|
||||
/><span i18n>Sign in with Google</span></a
|
||||
>
|
||||
</div>
|
||||
</ng-container>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div mat-dialog-actions>
|
||||
|
@ -10,16 +10,18 @@
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
<div *ngIf="isLoading" class="align-items-center d-flex">
|
||||
<ngx-skeleton-loader
|
||||
animation="pulse"
|
||||
class="mb-2"
|
||||
[theme]="{
|
||||
height: '4rem',
|
||||
width: '15rem'
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
@if (isLoading) {
|
||||
<div class="align-items-center d-flex">
|
||||
<ngx-skeleton-loader
|
||||
animation="pulse"
|
||||
class="mb-2"
|
||||
[theme]="{
|
||||
height: '4rem',
|
||||
width: '15rem'
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
<div
|
||||
class="display-4 font-weight-bold m-0 text-center value-container"
|
||||
[hidden]="isLoading"
|
||||
@ -34,28 +36,32 @@
|
||||
{{ unit }}
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="showDetails" class="row">
|
||||
<div class="d-flex col justify-content-end">
|
||||
<gf-value
|
||||
[colorizeSign]="true"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="
|
||||
isLoading ? undefined : performance?.netPerformanceWithCurrencyEffect
|
||||
"
|
||||
/>
|
||||
@if (showDetails) {
|
||||
<div class="row">
|
||||
<div class="d-flex col justify-content-end">
|
||||
<gf-value
|
||||
[colorizeSign]="true"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="
|
||||
isLoading
|
||||
? undefined
|
||||
: performance?.netPerformanceWithCurrencyEffect
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<gf-value
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="locale"
|
||||
[value]="
|
||||
isLoading
|
||||
? undefined
|
||||
: performance?.netPerformancePercentageWithCurrencyEffect
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<gf-value
|
||||
[colorizeSign]="true"
|
||||
[isPercent]="true"
|
||||
[locale]="locale"
|
||||
[value]="
|
||||
isLoading
|
||||
? undefined
|
||||
: performance?.netPerformancePercentageWithCurrencyEffect
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
@ -107,7 +107,9 @@
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
<div class="flex-grow-1 text-truncate" i18n>Fees</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<span *ngIf="summary?.fees || summary?.fees === 0" class="mr-1">-</span>
|
||||
@if (summary?.fees || summary?.fees === 0) {
|
||||
<span class="mr-1">-</span>
|
||||
}
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
[isCurrency]="true"
|
||||
@ -190,14 +192,27 @@
|
||||
<div class="flex-grow-1 text-truncate" i18n>Emergency Fund</div>
|
||||
<div
|
||||
class="align-items-center d-flex justify-content-end"
|
||||
[ngClass]="{ 'cursor-pointer': hasPermissionToUpdateUserSettings }"
|
||||
(click)="hasPermissionToUpdateUserSettings && onEditEmergencyFund()"
|
||||
[ngClass]="{
|
||||
'cursor-pointer':
|
||||
hasPermissionToUpdateUserSettings &&
|
||||
user?.subscription?.type !== 'Basic'
|
||||
}"
|
||||
(click)="
|
||||
hasPermissionToUpdateUserSettings &&
|
||||
user?.subscription?.type !== 'Basic' &&
|
||||
onEditEmergencyFund()
|
||||
"
|
||||
>
|
||||
<ion-icon
|
||||
*ngIf="hasPermissionToUpdateUserSettings && !isLoading"
|
||||
class="mr-1 text-muted"
|
||||
name="ellipsis-horizontal-circle-outline"
|
||||
/>
|
||||
@if (
|
||||
hasPermissionToUpdateUserSettings &&
|
||||
user?.subscription?.type !== 'Basic' &&
|
||||
!isLoading
|
||||
) {
|
||||
<ion-icon
|
||||
class="mr-1 text-muted"
|
||||
name="ellipsis-horizontal-circle-outline"
|
||||
/>
|
||||
}
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
[isCurrency]="true"
|
||||
@ -263,11 +278,9 @@
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
<div class="flex-grow-1 text-truncate" i18n>Liabilities</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<span
|
||||
*ngIf="summary?.liabilities || summary?.liabilities === 0"
|
||||
class="mr-1"
|
||||
>-</span
|
||||
>
|
||||
@if (summary?.liabilities || summary?.liabilities === 0) {
|
||||
<span class="mr-1">-</span>
|
||||
}
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
[isCurrency]="true"
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { getDateFnsLocale, getLocale } from '@ghostfolio/common/helper';
|
||||
import { PortfolioSummary } from '@ghostfolio/common/interfaces';
|
||||
import { PortfolioSummary, User } from '@ghostfolio/common/interfaces';
|
||||
import { translate } from '@ghostfolio/ui/i18n';
|
||||
|
||||
import {
|
||||
@ -26,6 +26,7 @@ export class PortfolioSummaryComponent implements OnChanges, OnInit {
|
||||
@Input() language: string;
|
||||
@Input() locale = getLocale();
|
||||
@Input() summary: PortfolioSummary;
|
||||
@Input() user: User;
|
||||
|
||||
@Output() emergencyFundChanged = new EventEmitter<number>();
|
||||
|
||||
|
@ -1,43 +1,51 @@
|
||||
<div class="py-3">
|
||||
<div class="align-items-center flex-nowrap no-gutters row">
|
||||
<div *ngIf="isLoading">
|
||||
<ngx-skeleton-loader
|
||||
animation="pulse"
|
||||
class="mr-2"
|
||||
[theme]="{
|
||||
height: '2rem',
|
||||
width: '2rem'
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
*ngIf="!isLoading"
|
||||
class="align-items-center d-flex icon-container mr-2 px-2"
|
||||
[ngClass]="{ okay: rule?.value === true, warn: rule?.value === false }"
|
||||
>
|
||||
<ion-icon *ngIf="rule?.value === true" name="checkmark-circle-outline" />
|
||||
<ion-icon *ngIf="rule?.value === false" name="warning-outline" />
|
||||
</div>
|
||||
<div *ngIf="isLoading" class="flex-grow-1">
|
||||
<ngx-skeleton-loader
|
||||
animation="pulse"
|
||||
class="mt-1 mb-1"
|
||||
[theme]="{
|
||||
height: '1rem',
|
||||
width: '10rem'
|
||||
}"
|
||||
/>
|
||||
<ngx-skeleton-loader
|
||||
animation="pulse"
|
||||
[theme]="{
|
||||
height: '1rem',
|
||||
width: '15rem'
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<div *ngIf="!isLoading" class="flex-grow-1">
|
||||
<div class="h6 my-1">{{ rule?.name }}</div>
|
||||
<div class="evaluation">{{ rule?.evaluation }}</div>
|
||||
</div>
|
||||
@if (isLoading) {
|
||||
<div>
|
||||
<ngx-skeleton-loader
|
||||
animation="pulse"
|
||||
class="mr-2"
|
||||
[theme]="{
|
||||
height: '2rem',
|
||||
width: '2rem'
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
} @else {
|
||||
<div
|
||||
class="align-items-center d-flex icon-container mr-2 px-2"
|
||||
[ngClass]="{ okay: rule?.value === true, warn: rule?.value === false }"
|
||||
>
|
||||
@if (rule?.value === true) {
|
||||
<ion-icon name="checkmark-circle-outline" />
|
||||
} @else {
|
||||
<ion-icon name="warning-outline" />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (isLoading) {
|
||||
<div class="flex-grow-1">
|
||||
<ngx-skeleton-loader
|
||||
animation="pulse"
|
||||
class="mt-1 mb-1"
|
||||
[theme]="{
|
||||
height: '1rem',
|
||||
width: '10rem'
|
||||
}"
|
||||
/>
|
||||
<ngx-skeleton-loader
|
||||
animation="pulse"
|
||||
[theme]="{
|
||||
height: '1rem',
|
||||
width: '15rem'
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="flex-grow-1">
|
||||
<div class="h6 my-1">{{ rule?.name }}</div>
|
||||
<div class="evaluation">{{ rule?.evaluation }}</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,20 +1,22 @@
|
||||
<div class="container p-0">
|
||||
<div class="row no-gutters">
|
||||
<div class="col">
|
||||
<mat-card
|
||||
*ngIf="hasPermissionToCreateOrder && rules === null"
|
||||
appearance="outlined"
|
||||
class="my-2 text-center"
|
||||
>
|
||||
<mat-card-content>
|
||||
<gf-no-transactions-info-indicator [hasBorder]="false" />
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
@if (hasPermissionToCreateOrder && rules === null) {
|
||||
<mat-card appearance="outlined" class="my-2 text-center">
|
||||
<mat-card-content>
|
||||
<gf-no-transactions-info-indicator [hasBorder]="false" />
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
|
||||
<gf-rule *ngIf="rules?.length === 0" [isLoading]="true" />
|
||||
<ng-container *ngIf="rules !== null && rules !== undefined">
|
||||
<gf-rule *ngFor="let rule of rules" [rule]="rule" />
|
||||
</ng-container>
|
||||
@if (rules?.length === 0) {
|
||||
<gf-rule [isLoading]="true" />
|
||||
}
|
||||
@if (rules !== null && rules !== undefined) {
|
||||
@for (rule of rules; track rule) {
|
||||
<gf-rule [rule]="rule" />
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,12 +3,13 @@
|
||||
[formControl]="optionFormControl"
|
||||
(change)="onValueChange()"
|
||||
>
|
||||
<mat-radio-button
|
||||
*ngFor="let option of options"
|
||||
class="d-inline-flex"
|
||||
[disabled]="isLoading"
|
||||
[ngClass]="{ 'cursor-pointer': !isLoading }"
|
||||
[value]="option.value"
|
||||
>{{ option.label }}</mat-radio-button
|
||||
>
|
||||
@for (option of options; track option) {
|
||||
<mat-radio-button
|
||||
class="d-inline-flex"
|
||||
[disabled]="isLoading"
|
||||
[ngClass]="{ 'cursor-pointer': !isLoading }"
|
||||
[value]="option.value"
|
||||
>{{ option.label }}</mat-radio-button
|
||||
>
|
||||
}
|
||||
</mat-radio-group>
|
||||
|
@ -3,25 +3,26 @@
|
||||
class="align-items-center d-none d-sm-flex h3 justify-content-center mb-3 text-center"
|
||||
>
|
||||
<span i18n>Granted Access</span>
|
||||
<gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
/>
|
||||
@if (user?.subscription?.type === 'Basic') {
|
||||
<gf-premium-indicator class="ml-1" />
|
||||
}
|
||||
</h1>
|
||||
<gf-access-table
|
||||
[accesses]="accesses"
|
||||
[showActions]="hasPermissionToDeleteAccess"
|
||||
(accessDeleted)="onDeleteAccess($event)"
|
||||
/>
|
||||
<div *ngIf="hasPermissionToCreateAccess" class="fab-container">
|
||||
<a
|
||||
class="align-items-center d-flex justify-content-center"
|
||||
color="primary"
|
||||
mat-fab
|
||||
[queryParams]="{ createDialog: true }"
|
||||
[routerLink]="[]"
|
||||
>
|
||||
<ion-icon name="add-outline" size="large" />
|
||||
</a>
|
||||
</div>
|
||||
@if (hasPermissionToCreateAccess) {
|
||||
<div class="fab-container">
|
||||
<a
|
||||
class="align-items-center d-flex justify-content-center"
|
||||
color="primary"
|
||||
mat-fab
|
||||
[queryParams]="{ createDialog: true }"
|
||||
[routerLink]="[]"
|
||||
>
|
||||
<ion-icon name="add-outline" size="large" />
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
@ -1,10 +1,6 @@
|
||||
:host {
|
||||
color: rgb(var(--dark-primary-text));
|
||||
display: block;
|
||||
|
||||
gf-access-table {
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
|
@ -6,64 +6,57 @@
|
||||
[expiresAt]="user?.subscription?.expiresAt | date: defaultDateFormat"
|
||||
[name]="user?.subscription?.type"
|
||||
/>
|
||||
<div
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="d-flex flex-column mt-5"
|
||||
>
|
||||
<ng-container
|
||||
*ngIf="
|
||||
@if (user?.subscription?.type === 'Basic') {
|
||||
<div class="d-flex flex-column mt-5">
|
||||
@if (
|
||||
hasPermissionForSubscription && hasPermissionToUpdateUserSettings
|
||||
"
|
||||
>
|
||||
<button color="primary" mat-flat-button (click)="onCheckout()">
|
||||
<ng-container *ngIf="user.subscription.offer === 'default'" i18n
|
||||
>Upgrade Plan</ng-container
|
||||
>
|
||||
<ng-container
|
||||
*ngIf="
|
||||
) {
|
||||
<button color="primary" mat-flat-button (click)="onCheckout()">
|
||||
@if (user.subscription.offer === 'default') {
|
||||
<ng-container i18n>Upgrade Plan</ng-container>
|
||||
} @else if (
|
||||
user.subscription.offer === 'renewal' ||
|
||||
user.subscription.offer === 'renewal-early-bird'
|
||||
"
|
||||
i18n
|
||||
>Renew Plan</ng-container
|
||||
>
|
||||
</button>
|
||||
<div *ngIf="price" class="mt-1 text-center">
|
||||
<ng-container *ngIf="coupon"
|
||||
><del class="text-muted"
|
||||
>{{ baseCurrency }} {{ price }}</del
|
||||
> {{ baseCurrency }} {{
|
||||
price - coupon
|
||||
}}</ng-container
|
||||
>
|
||||
<ng-container *ngIf="!coupon"
|
||||
>{{ baseCurrency }} {{ price }}</ng-container
|
||||
> <span i18n>per year</span>
|
||||
) {
|
||||
<ng-container i18n>Renew Plan</ng-container>
|
||||
}
|
||||
</button>
|
||||
@if (price) {
|
||||
<div class="mt-1 text-center">
|
||||
@if (coupon) {
|
||||
<del class="text-muted"
|
||||
>{{ baseCurrency }} {{ price }}</del
|
||||
> {{ baseCurrency }} {{ price - coupon }}
|
||||
} @else {
|
||||
{{ baseCurrency }} {{ price }}
|
||||
}
|
||||
<span i18n>per year</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
<div class="align-items-center d-flex justify-content-center mt-4">
|
||||
@if (!user?.subscription?.expiresAt) {
|
||||
<a class="mx-1" mat-stroked-button [href]="trySubscriptionMail"
|
||||
><span i18n>Try Premium</span>
|
||||
<gf-premium-indicator
|
||||
class="d-inline-block ml-1"
|
||||
[enableLink]="false"
|
||||
/>
|
||||
</a>
|
||||
}
|
||||
@if (hasPermissionToUpdateUserSettings) {
|
||||
<a
|
||||
class="mx-1"
|
||||
i18n
|
||||
mat-stroked-button
|
||||
[routerLink]=""
|
||||
(click)="onRedeemCoupon()"
|
||||
>Redeem Coupon</a
|
||||
>
|
||||
}
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="align-items-center d-flex justify-content-center mt-4">
|
||||
<a
|
||||
*ngIf="!user?.subscription?.expiresAt"
|
||||
class="mx-1"
|
||||
mat-stroked-button
|
||||
[href]="trySubscriptionMail"
|
||||
><span i18n>Try Premium</span>
|
||||
<gf-premium-indicator
|
||||
class="d-inline-block ml-1"
|
||||
[enableLink]="false"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
*ngIf="hasPermissionToUpdateUserSettings"
|
||||
class="mx-1"
|
||||
i18n
|
||||
mat-stroked-button
|
||||
[routerLink]=""
|
||||
(click)="onRedeemCoupon()"
|
||||
>Redeem Coupon</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -20,9 +20,10 @@ import {
|
||||
} from '@angular/core';
|
||||
import { FormBuilder, Validators } from '@angular/forms';
|
||||
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { format, parseISO } from 'date-fns';
|
||||
import { uniq } from 'lodash';
|
||||
import { EMPTY, Subject } from 'rxjs';
|
||||
import { EMPTY, Subject, throwError } from 'rxjs';
|
||||
import { catchError, takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
@ -42,6 +43,7 @@ export class UserAccountSettingsComponent implements OnDestroy, OnInit {
|
||||
public hasPermissionToUpdateViewMode: boolean;
|
||||
public hasPermissionToUpdateUserSettings: boolean;
|
||||
public isAccessTokenHidden = true;
|
||||
public isFingerprintSupported = this.doesBrowserSupportAuthn();
|
||||
public isWebAuthnEnabled: boolean;
|
||||
public language = document.documentElement.lang;
|
||||
public locales = [
|
||||
@ -67,6 +69,7 @@ export class UserAccountSettingsComponent implements OnDestroy, OnInit {
|
||||
private dataService: DataService,
|
||||
private formBuilder: FormBuilder,
|
||||
private settingsStorageService: SettingsStorageService,
|
||||
private snackBar: MatSnackBar,
|
||||
private tokenStorageService: TokenStorageService,
|
||||
private userService: UserService,
|
||||
public webAuthnService: WebAuthnService
|
||||
@ -222,9 +225,15 @@ export class UserAccountSettingsComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
public onSignInWithFingerprintChange(aEvent: MatSlideToggleChange) {
|
||||
public async onSignInWithFingerprintChange(aEvent: MatSlideToggleChange) {
|
||||
if (aEvent.checked) {
|
||||
this.registerDevice();
|
||||
try {
|
||||
await this.registerDevice();
|
||||
} catch {
|
||||
aEvent.source.checked = false;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
} else {
|
||||
const confirmation = confirm(
|
||||
$localize`Do you really want to remove this sign in method?`
|
||||
@ -265,35 +274,54 @@ export class UserAccountSettingsComponent implements OnDestroy, OnInit {
|
||||
this.webAuthnService
|
||||
.deregister()
|
||||
.pipe(
|
||||
takeUntil(this.unsubscribeSubject),
|
||||
catchError(() => {
|
||||
this.update();
|
||||
|
||||
return EMPTY;
|
||||
})
|
||||
}),
|
||||
takeUntil(this.unsubscribeSubject)
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.update();
|
||||
});
|
||||
}
|
||||
|
||||
private registerDevice() {
|
||||
this.webAuthnService
|
||||
.register()
|
||||
.pipe(
|
||||
takeUntil(this.unsubscribeSubject),
|
||||
catchError(() => {
|
||||
this.update();
|
||||
private doesBrowserSupportAuthn() {
|
||||
// Authn is built on top of PublicKeyCredential: https://stackoverflow.com/a/55868189
|
||||
return typeof PublicKeyCredential !== 'undefined';
|
||||
}
|
||||
|
||||
return EMPTY;
|
||||
})
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.settingsStorageService.removeSetting(KEY_STAY_SIGNED_IN);
|
||||
this.settingsStorageService.removeSetting(KEY_TOKEN);
|
||||
private registerDevice(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.webAuthnService
|
||||
.register()
|
||||
.pipe(
|
||||
catchError((error: Error) => {
|
||||
this.snackBar.open(
|
||||
$localize`Oops! There was an error setting up biometric authentication.`,
|
||||
undefined,
|
||||
{ duration: 3000 }
|
||||
);
|
||||
|
||||
this.update();
|
||||
});
|
||||
return throwError(() => {
|
||||
return error;
|
||||
});
|
||||
}),
|
||||
takeUntil(this.unsubscribeSubject)
|
||||
)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.settingsStorageService.removeSetting(KEY_STAY_SIGNED_IN);
|
||||
this.settingsStorageService.removeSetting(KEY_TOKEN);
|
||||
|
||||
this.update();
|
||||
resolve();
|
||||
},
|
||||
error: (error) => {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private update() {
|
||||
|
@ -36,11 +36,9 @@
|
||||
onChangeUserSetting('baseCurrency', $event.value)
|
||||
"
|
||||
>
|
||||
<mat-option
|
||||
*ngFor="let currency of currencies"
|
||||
[value]="currency"
|
||||
>{{ currency }}</mat-option
|
||||
>
|
||||
@for (currency of currencies; track currency) {
|
||||
<mat-option [value]="currency">{{ currency }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
@ -48,20 +46,18 @@
|
||||
<div class="align-items-center d-flex mb-2">
|
||||
<div class="pr-1 w-50">
|
||||
<div i18n>Language</div>
|
||||
<div
|
||||
*ngIf="isCommunityLanguage()"
|
||||
class="hint-text text-muted"
|
||||
i18n
|
||||
>
|
||||
If a translation is missing, kindly support us in extending it
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio/blob/main/apps/client/src/locales/messages.{{
|
||||
language
|
||||
}}.xlf"
|
||||
target="_blank"
|
||||
>here</a
|
||||
>.
|
||||
</div>
|
||||
@if (isCommunityLanguage()) {
|
||||
<div class="hint-text text-muted" i18n>
|
||||
If a translation is missing, kindly support us in extending it
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio/blob/main/apps/client/src/locales/messages.{{
|
||||
language
|
||||
}}.xlf"
|
||||
target="_blank"
|
||||
>here</a
|
||||
>.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="pl-1 w-50">
|
||||
<mat-form-field appearance="outline" class="w-100 without-hint">
|
||||
@ -134,9 +130,9 @@
|
||||
"
|
||||
>
|
||||
<mat-option [value]="null" />
|
||||
<mat-option *ngFor="let locale of locales" [value]="locale">{{
|
||||
locale
|
||||
}}</mat-option>
|
||||
@for (locale of locales; track locale) {
|
||||
<mat-option [value]="locale">{{ locale }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
@ -193,31 +189,32 @@
|
||||
color="primary"
|
||||
hideIcon="true"
|
||||
[checked]="isWebAuthnEnabled === true"
|
||||
[disabled]="!hasPermissionToUpdateUserSettings"
|
||||
[disabled]="
|
||||
!hasPermissionToUpdateUserSettings || !isFingerprintSupported
|
||||
"
|
||||
(change)="onSignInWithFingerprintChange($event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
*ngIf="hasPermissionToUpdateUserSettings"
|
||||
class="align-items-center d-flex mt-4 py-1"
|
||||
>
|
||||
<div class="pr-1 w-50">
|
||||
<div i18n>Experimental Features</div>
|
||||
<div class="hint-text text-muted" i18n>
|
||||
Sneak peek at upcoming functionality
|
||||
@if (hasPermissionToUpdateUserSettings) {
|
||||
<div class="align-items-center d-flex mt-4 py-1">
|
||||
<div class="pr-1 w-50">
|
||||
<div i18n>Experimental Features</div>
|
||||
<div class="hint-text text-muted" i18n>
|
||||
Sneak peek at upcoming functionality
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-1 w-50">
|
||||
<mat-slide-toggle
|
||||
color="primary"
|
||||
hideIcon="true"
|
||||
[checked]="user.settings.isExperimentalFeatures"
|
||||
[disabled]="!hasPermissionToUpdateUserSettings"
|
||||
(change)="onExperimentalFeaturesChange($event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pl-1 w-50">
|
||||
<mat-slide-toggle
|
||||
color="primary"
|
||||
hideIcon="true"
|
||||
[checked]="user.settings.isExperimentalFeatures"
|
||||
[disabled]="!hasPermissionToUpdateUserSettings"
|
||||
(change)="onExperimentalFeaturesChange($event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="align-items-center d-flex mt-4 py-1">
|
||||
<div class="pr-1 w-50">
|
||||
Ghostfolio <ng-container i18n>User ID</ng-container>
|
||||
|
@ -1,10 +1,11 @@
|
||||
<ngx-skeleton-loader
|
||||
*ngIf="isLoading"
|
||||
animation="pulse"
|
||||
class="h-100"
|
||||
[theme]="{
|
||||
width: '100%'
|
||||
}"
|
||||
/>
|
||||
@if (isLoading) {
|
||||
<ngx-skeleton-loader
|
||||
animation="pulse"
|
||||
class="h-100"
|
||||
[theme]="{
|
||||
width: '100%'
|
||||
}"
|
||||
/>
|
||||
}
|
||||
|
||||
<div class="align-items-center d-flex h-100 w-100" id="svgMap"></div>
|
||||
|
@ -2,7 +2,6 @@ import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
|
||||
import { InfoItem } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
|
||||
import {
|
||||
HTTP_INTERCEPTORS,
|
||||
@ -25,7 +24,6 @@ import { catchError, tap } from 'rxjs/operators';
|
||||
|
||||
@Injectable()
|
||||
export class HttpResponseInterceptor implements HttpInterceptor {
|
||||
public hasPermissionForSubscription: boolean;
|
||||
public info: InfoItem;
|
||||
public snackBarRef: MatSnackBarRef<TextOnlySnackBar>;
|
||||
|
||||
@ -37,11 +35,6 @@ export class HttpResponseInterceptor implements HttpInterceptor {
|
||||
private webAuthnService: WebAuthnService
|
||||
) {
|
||||
this.info = this.dataService.fetchInfo();
|
||||
|
||||
this.hasPermissionForSubscription = hasPermission(
|
||||
this.info?.globalPermissions,
|
||||
permissions.enableSubscription
|
||||
);
|
||||
}
|
||||
|
||||
public intercept(
|
||||
@ -65,12 +58,8 @@ export class HttpResponseInterceptor implements HttpInterceptor {
|
||||
);
|
||||
} else if (!error.url.includes('/auth')) {
|
||||
this.snackBarRef = this.snackBar.open(
|
||||
this.hasPermissionForSubscription
|
||||
? $localize`This feature requires a subscription.`
|
||||
: $localize`This action is not allowed.`,
|
||||
this.hasPermissionForSubscription
|
||||
? $localize`Upgrade Plan`
|
||||
: undefined,
|
||||
$localize`This action is not allowed.`,
|
||||
undefined,
|
||||
{ duration: 6000 }
|
||||
);
|
||||
}
|
||||
|
@ -8,22 +8,23 @@
|
||||
[disablePagination]="true"
|
||||
[tabPanel]="tabPanel"
|
||||
>
|
||||
<ng-container *ngFor="let tab of tabs">
|
||||
<a
|
||||
#rla="routerLinkActive"
|
||||
*ngIf="tab.showCondition !== false"
|
||||
class="no-min-width px-3"
|
||||
mat-tab-link
|
||||
routerLinkActive
|
||||
[active]="rla.isActive"
|
||||
[routerLink]="tab.path"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
>
|
||||
<ion-icon
|
||||
[name]="tab.iconName"
|
||||
[size]="deviceType === 'mobile' ? 'large' : 'small'"
|
||||
/>
|
||||
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
|
||||
</a>
|
||||
</ng-container>
|
||||
@for (tab of tabs; track tab) {
|
||||
@if (tab.showCondition !== false) {
|
||||
<a
|
||||
#rla="routerLinkActive"
|
||||
class="no-min-width px-3"
|
||||
mat-tab-link
|
||||
routerLinkActive
|
||||
[active]="rla.isActive"
|
||||
[routerLink]="tab.path"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
>
|
||||
<ion-icon
|
||||
[name]="tab.iconName"
|
||||
[size]="deviceType === 'mobile' ? 'large' : 'small'"
|
||||
/>
|
||||
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
|
||||
</a>
|
||||
}
|
||||
}
|
||||
</nav>
|
||||
|
@ -21,11 +21,12 @@
|
||||
title="GNU Affero General Public License"
|
||||
>AGPL-3.0 license</a
|
||||
>
|
||||
<ng-container *ngIf="hasPermissionForStatistics">
|
||||
@if (hasPermissionForStatistics) {
|
||||
and we share aggregated
|
||||
<a title="Open Startup" [routerLink]="['/open']">key metrics</a>
|
||||
of the platform’s performance</ng-container
|
||||
>. The project has been initiated by
|
||||
of the platform’s performance
|
||||
}
|
||||
. The project has been initiated by
|
||||
<a href="https://dotsilver.ch" title="Website of Thomas Kaul"
|
||||
>Thomas Kaul</a
|
||||
>
|
||||
@ -35,12 +36,12 @@
|
||||
title="Contributors to Ghostfolio"
|
||||
>contributors</a
|
||||
>.
|
||||
<ng-container *ngIf="hasPermissionForSubscription"
|
||||
>Check the system status at
|
||||
@if (hasPermissionForSubscription) {
|
||||
Check the system status at
|
||||
<a href="https://status.ghostfol.io" title="Ghostfolio Status"
|
||||
>status.ghostfol.io</a
|
||||
>.</ng-container
|
||||
>
|
||||
>.
|
||||
}
|
||||
</p>
|
||||
<p>
|
||||
If you encounter a bug or would like to suggest an improvement or a
|
||||
@ -54,15 +55,16 @@
|
||||
>
|
||||
community, post to
|
||||
<a
|
||||
href="https://twitter.com/ghostfolio_"
|
||||
href="https://x.com/ghostfolio_"
|
||||
title="Post to Ghostfolio on X (formerly Twitter)"
|
||||
>@ghostfolio_</a
|
||||
><ng-container *ngIf="user?.subscription?.type === 'Premium'"
|
||||
>, send an e-mail to
|
||||
>
|
||||
@if (user?.subscription?.type === 'Premium') {
|
||||
, send an e-mail to
|
||||
<a href="mailto:hi@ghostfol.io" title="Send an e-mail"
|
||||
>hi@ghostfol.io</a
|
||||
></ng-container
|
||||
>
|
||||
>
|
||||
}
|
||||
or start a discussion at
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio"
|
||||
@ -73,21 +75,22 @@
|
||||
<p class="align-items-center d-flex justify-content-center">
|
||||
<a
|
||||
class="mx-2"
|
||||
href="https://twitter.com/ghostfolio_"
|
||||
href="https://x.com/ghostfolio_"
|
||||
mat-icon-button
|
||||
title="Follow Ghostfolio on X (formerly Twitter)"
|
||||
>
|
||||
<ion-icon name="logo-x" />
|
||||
</a>
|
||||
<a
|
||||
*ngIf="user?.subscription?.type === 'Premium'"
|
||||
class="mx-2"
|
||||
href="mailto:hi@ghostfol.io"
|
||||
mat-icon-button
|
||||
title="Send an e-mail"
|
||||
>
|
||||
<ion-icon name="mail" />
|
||||
</a>
|
||||
@if (user?.subscription?.type === 'Premium') {
|
||||
<a
|
||||
class="mx-2"
|
||||
href="mailto:hi@ghostfol.io"
|
||||
mat-icon-button
|
||||
title="Send an e-mail"
|
||||
>
|
||||
<ion-icon name="mail" />
|
||||
</a>
|
||||
}
|
||||
<a
|
||||
class="mx-2"
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
@ -105,29 +108,26 @@
|
||||
<ion-icon name="logo-github" />
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
class="d-flex justify-content-center"
|
||||
>
|
||||
<div
|
||||
class="independent-and-bootstrapped-logo mb-2"
|
||||
title="Ghostfolio is an independent & bootstrapped business"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
*ngIf="!hasPermissionForSubscription"
|
||||
class="d-flex justify-content-center"
|
||||
>
|
||||
<a
|
||||
href="https://www.buymeacoffee.com/ghostfolio"
|
||||
target="_blank"
|
||||
title="Support Ghostfolio"
|
||||
><img
|
||||
class="mb-2"
|
||||
src="../assets/images/button-buy-me-a-coffee.png"
|
||||
width="180"
|
||||
/></a>
|
||||
</div>
|
||||
@if (hasPermissionForSubscription) {
|
||||
<div class="d-flex justify-content-center">
|
||||
<div
|
||||
class="independent-and-bootstrapped-logo mb-2"
|
||||
title="Ghostfolio is an independent & bootstrapped business"
|
||||
></div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="d-flex justify-content-center">
|
||||
<a
|
||||
href="https://www.buymeacoffee.com/ghostfolio"
|
||||
target="_blank"
|
||||
title="Support Ghostfolio"
|
||||
><img
|
||||
class="mb-2"
|
||||
src="../assets/images/button-buy-me-a-coffee.png"
|
||||
width="180"
|
||||
/></a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -2,44 +2,41 @@
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>Accounts</h1>
|
||||
<div class="accounts">
|
||||
<gf-accounts-table
|
||||
[accounts]="accounts"
|
||||
[baseCurrency]="user?.settings?.baseCurrency"
|
||||
[deviceType]="deviceType"
|
||||
[locale]="user?.settings?.locale"
|
||||
[showActions]="
|
||||
!hasImpersonationId &&
|
||||
hasPermissionToUpdateAccount &&
|
||||
!user.settings.isRestrictedView
|
||||
"
|
||||
[totalBalanceInBaseCurrency]="totalBalanceInBaseCurrency"
|
||||
[totalValueInBaseCurrency]="totalValueInBaseCurrency"
|
||||
[transactionCount]="transactionCount"
|
||||
(accountDeleted)="onDeleteAccount($event)"
|
||||
(accountToUpdate)="onUpdateAccount($event)"
|
||||
(transferBalance)="onTransferBalance()"
|
||||
/>
|
||||
</div>
|
||||
<gf-accounts-table
|
||||
[accounts]="accounts"
|
||||
[baseCurrency]="user?.settings?.baseCurrency"
|
||||
[deviceType]="deviceType"
|
||||
[locale]="user?.settings?.locale"
|
||||
[showActions]="
|
||||
!hasImpersonationId &&
|
||||
hasPermissionToUpdateAccount &&
|
||||
!user.settings.isRestrictedView
|
||||
"
|
||||
[totalBalanceInBaseCurrency]="totalBalanceInBaseCurrency"
|
||||
[totalValueInBaseCurrency]="totalValueInBaseCurrency"
|
||||
[transactionCount]="transactionCount"
|
||||
(accountDeleted)="onDeleteAccount($event)"
|
||||
(accountToUpdate)="onUpdateAccount($event)"
|
||||
(transferBalance)="onTransferBalance()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
*ngIf="
|
||||
!hasImpersonationId &&
|
||||
hasPermissionToCreateAccount &&
|
||||
!user.settings.isRestrictedView
|
||||
"
|
||||
class="fab-container"
|
||||
>
|
||||
<a
|
||||
class="align-items-center d-flex justify-content-center"
|
||||
color="primary"
|
||||
mat-fab
|
||||
[queryParams]="{ createDialog: true }"
|
||||
[routerLink]="[]"
|
||||
>
|
||||
<ion-icon name="add-outline" size="large" />
|
||||
</a>
|
||||
</div>
|
||||
@if (
|
||||
!hasImpersonationId &&
|
||||
hasPermissionToCreateAccount &&
|
||||
!user.settings.isRestrictedView
|
||||
) {
|
||||
<div class="fab-container">
|
||||
<a
|
||||
class="align-items-center d-flex justify-content-center"
|
||||
color="primary"
|
||||
mat-fab
|
||||
[queryParams]="{ createDialog: true }"
|
||||
[routerLink]="[]"
|
||||
>
|
||||
<ion-icon name="add-outline" size="large" />
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user