Compare commits
43 Commits
Author | SHA1 | Date | |
---|---|---|---|
c8682a7393 | |||
144b6b2211 | |||
16a5ace4be | |||
b24ddc30c9 | |||
19333ab084 | |||
7529a7a26c | |||
21ebaae6ef | |||
3bc8b3c836 | |||
bb9415cc15 | |||
b3baeb8a5d | |||
1f393e78f6 | |||
215f5eafa6 | |||
1916e5343d | |||
fa9863fc54 | |||
7bf48ef351 | |||
faef3606fd | |||
d0ccd4d238 | |||
51e3650790 | |||
db29e2b666 | |||
655a68a847 | |||
86296b3591 | |||
73c127f10c | |||
cf4c981cd9 | |||
1b9541b933 | |||
5bca8de44e | |||
136c4bf50b | |||
4d700e3b83 | |||
740fa6fc84 | |||
cdb8dc72c7 | |||
4b3afb5c97 | |||
abf208432a | |||
19e6df4fb2 | |||
7fc3fff431 | |||
edd690850c | |||
302339e1cd | |||
739796bc79 | |||
9c30139b86 | |||
0af528b649 | |||
9636c87a2e | |||
ad46fb6d61 | |||
8e000baef2 | |||
a2e1209196 | |||
ef4a75d1f0 |
@ -12,5 +12,5 @@ POSTGRES_PASSWORD=<INSERT_POSTGRES_PASSWORD>
|
||||
|
||||
ACCESS_TOKEN_SALT=<INSERT_RANDOM_STRING>
|
||||
ALPHA_VANTAGE_API_KEY=
|
||||
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?sslmode=prefer
|
||||
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?connect_timeout=300&sslmode=prefer
|
||||
JWT_SECRET_KEY=<INSERT_RANDOM_STRING>
|
||||
|
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -36,6 +36,9 @@ The Issue tracker is **ONLY** used for reporting bugs. New features should be di
|
||||
|
||||
<!-- Please complete the following information -->
|
||||
|
||||
- [ ] Cloud
|
||||
- [ ] Self-hosted
|
||||
|
||||
- Ghostfolio Version X.Y.Z
|
||||
- Browser
|
||||
- OS
|
||||
|
@ -1,15 +0,0 @@
|
||||
module.exports = {
|
||||
docs: {
|
||||
autodocs: true
|
||||
},
|
||||
framework: {
|
||||
name: '@storybook/angular',
|
||||
options: {}
|
||||
}
|
||||
// uncomment the property below if you want to apply some webpack config globally
|
||||
// webpackFinal: async (config, { configType }) => {
|
||||
// // Make whatever fine-grained changes you need that should apply to all storybook configs
|
||||
// // Return the altered config
|
||||
// return config;
|
||||
// },
|
||||
};
|
@ -1,10 +0,0 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"exclude": [
|
||||
"../**/*.spec.js",
|
||||
"../**/*.spec.ts",
|
||||
"../**/*.spec.tsx",
|
||||
"../**/*.spec.jsx"
|
||||
],
|
||||
"include": ["../**/*"]
|
||||
}
|
27
.vscode/launch.json
vendored
27
.vscode/launch.json
vendored
@ -2,32 +2,33 @@
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug Jest File",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/node_modules/@nrwl/cli/bin/nx",
|
||||
"args": [
|
||||
"test",
|
||||
"--codeCoverage=false",
|
||||
"--testFile=${workspaceFolder}/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts"
|
||||
],
|
||||
"console": "internalConsole",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "internalConsole"
|
||||
"name": "Debug Jest",
|
||||
"program": "${workspaceFolder}/node_modules/@nrwl/cli/bin/nx",
|
||||
"request": "launch",
|
||||
"type": "node"
|
||||
},
|
||||
{
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch Program",
|
||||
"program": "${workspaceFolder}/apps/api/src/main.ts",
|
||||
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
|
||||
"outFiles": ["${workspaceFolder}/dist/apps/api/**/*.js"],
|
||||
"autoAttachChildProcesses": true,
|
||||
"console": "integratedTerminal",
|
||||
"cwd": "${workspaceFolder}/apps/api",
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"name": "Debug API",
|
||||
"outFiles": ["${workspaceFolder}/dist/apps/api/**/*.js"],
|
||||
"program": "${workspaceFolder}/apps/api/src/main.ts",
|
||||
"request": "launch",
|
||||
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
|
||||
"skipFiles": [
|
||||
"${workspaceFolder}/node_modules/**/*.js",
|
||||
"<node_internals>/**/*.js"
|
||||
],
|
||||
"console": "integratedTerminal"
|
||||
"type": "node"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
82
CHANGELOG.md
82
CHANGELOG.md
@ -5,6 +5,86 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## 1.273.0 - 2023-05-28
|
||||
|
||||
### Added
|
||||
|
||||
- Added a stepper to the activities import dialog
|
||||
- Added a link to manage the benchmarks to the benchmark comparator
|
||||
- Added support for localized routes
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue in the data source transformation
|
||||
|
||||
## 1.272.0 - 2023-05-26
|
||||
|
||||
### Added
|
||||
|
||||
- Added support to set an asset profile as a benchmark
|
||||
|
||||
### Changed
|
||||
|
||||
- Decreased the density of the `@angular/material` tables
|
||||
- Improved the portfolio proportion chart component by supporting case insensitive names
|
||||
- Improved the breadcrumb navigation style in the blog post pages for mobile
|
||||
- Improved the error handling in the delete user endpoint
|
||||
- Improved the style of the _Changelog & License_ button on the about page
|
||||
- Upgraded `ionicons` from version `6.1.2` to `7.1.0`
|
||||
|
||||
## 1.271.0 - 2023-05-20
|
||||
|
||||
### Added
|
||||
|
||||
- Added the historical data and search functionality for the `FINANCIAL_MODELING_PREP` data source type
|
||||
- Added a blog post: _Unlock your Financial Potential with Ghostfolio_
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the local number formatting in the value component
|
||||
- Changed the uptime to the last 90 days on the _Open Startup_ (`/open`) page
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the vertical alignment in the toggle component
|
||||
|
||||
## 1.270.1 - 2023-05-19
|
||||
|
||||
### Added
|
||||
|
||||
- Added the cash balance and the value of equity to the account detail dialog
|
||||
- Added a check for duplicates to the preview step of the import dividends dialog
|
||||
- Added an error message for duplicates to the preview step of the activities import
|
||||
- Added a connection timeout to the environment variable `DATABASE_URL`
|
||||
- Introduced the _Open Startup_ (`/open`) page with aggregated key metrics including uptime
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the mobile layout of the portfolio summary tab on the home page
|
||||
- Improved the language localization for German (`de`)
|
||||
- Upgraded `prisma` from version `4.13.0` to `4.14.1`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improved the _Select all_ activities checkbox state after importing activities including a duplicate
|
||||
- Fixed an issue with the data source transformation in the import dividends dialog
|
||||
- Fixed the _Storybook_ setup
|
||||
|
||||
## 1.269.0 - 2023-05-11
|
||||
|
||||
### Added
|
||||
|
||||
- Added `FINANCIAL_MODELING_PREP` as a new data source type
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the market price on the first buy date in the chart of the position detail dialog
|
||||
- Restructured the admin control panel with a new settings tab
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an error that occurred while editing an activity caused by the cash balance update
|
||||
|
||||
## 1.268.0 - 2023-05-08
|
||||
|
||||
### Added
|
||||
@ -210,7 +290,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
- Changed the slide toggles to checkboxes on the account page
|
||||
- Changed the slide toggles to checkboxes in the admin control panel
|
||||
- Decreased the density of the theme
|
||||
- Increased the density of the theme
|
||||
- Migrated the style of various components to `@angular/material` `15` (mdc)
|
||||
- Upgraded `@angular/cdk` and `@angular/material` from version `15.2.5` to `15.2.6`
|
||||
- Upgraded `bull` from version `4.10.2` to `4.10.4`
|
||||
|
@ -1,24 +1,36 @@
|
||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
||||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
||||
import {
|
||||
import type {
|
||||
BenchmarkMarketDataDetails,
|
||||
BenchmarkResponse
|
||||
BenchmarkResponse,
|
||||
UniqueAsset
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpException,
|
||||
Inject,
|
||||
Param,
|
||||
Post,
|
||||
UseGuards,
|
||||
UseInterceptors
|
||||
} from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { DataSource } from '@prisma/client';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
|
||||
import { BenchmarkService } from './benchmark.service';
|
||||
|
||||
@Controller('benchmark')
|
||||
export class BenchmarkController {
|
||||
public constructor(private readonly benchmarkService: BenchmarkService) {}
|
||||
public constructor(
|
||||
private readonly benchmarkService: BenchmarkService,
|
||||
@Inject(REQUEST) private readonly request: RequestWithUser
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||
@ -45,4 +57,41 @@ export class BenchmarkController {
|
||||
symbol
|
||||
});
|
||||
}
|
||||
|
||||
@Post()
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async addBenchmark(@Body() { dataSource, symbol }: UniqueAsset) {
|
||||
if (
|
||||
!hasPermission(
|
||||
this.request.user.permissions,
|
||||
permissions.accessAdminControl
|
||||
)
|
||||
) {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||
StatusCodes.FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const benchmark = await this.benchmarkService.addBenchmark({
|
||||
dataSource,
|
||||
symbol
|
||||
});
|
||||
|
||||
if (!benchmark) {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.NOT_FOUND),
|
||||
StatusCodes.NOT_FOUND
|
||||
);
|
||||
}
|
||||
|
||||
return benchmark;
|
||||
} catch {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR),
|
||||
StatusCodes.INTERNAL_SERVER_ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module';
|
||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-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 { Module } from '@nestjs/common';
|
||||
@ -17,6 +18,7 @@ import { BenchmarkService } from './benchmark.service';
|
||||
ConfigurationModule,
|
||||
DataProviderModule,
|
||||
MarketDataModule,
|
||||
PrismaModule,
|
||||
PropertyModule,
|
||||
RedisCacheModule,
|
||||
SymbolModule,
|
||||
|
@ -4,7 +4,15 @@ describe('BenchmarkService', () => {
|
||||
let benchmarkService: BenchmarkService;
|
||||
|
||||
beforeAll(async () => {
|
||||
benchmarkService = new BenchmarkService(null, null, null, null, null, null);
|
||||
benchmarkService = new BenchmarkService(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
it('calculateChangeInPercentage', async () => {
|
||||
|
@ -2,6 +2,7 @@ import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.s
|
||||
import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service';
|
||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
||||
import {
|
||||
@ -11,6 +12,7 @@ import {
|
||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
BenchmarkMarketDataDetails,
|
||||
BenchmarkProperty,
|
||||
BenchmarkResponse,
|
||||
UniqueAsset
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
@ -18,6 +20,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { SymbolProfile } from '@prisma/client';
|
||||
import Big from 'big.js';
|
||||
import { format } from 'date-fns';
|
||||
import { uniqBy } from 'lodash';
|
||||
import ms from 'ms';
|
||||
|
||||
@Injectable()
|
||||
@ -27,6 +30,7 @@ export class BenchmarkService {
|
||||
public constructor(
|
||||
private readonly dataProviderService: DataProviderService,
|
||||
private readonly marketDataService: MarketDataService,
|
||||
private readonly prismaService: PrismaService,
|
||||
private readonly propertyService: PropertyService,
|
||||
private readonly redisCacheService: RedisCacheService,
|
||||
private readonly symbolProfileService: SymbolProfileService,
|
||||
@ -116,9 +120,9 @@ export class BenchmarkService {
|
||||
|
||||
public async getBenchmarkAssetProfiles(): Promise<Partial<SymbolProfile>[]> {
|
||||
const symbolProfileIds: string[] = (
|
||||
((await this.propertyService.getByKey(PROPERTY_BENCHMARKS)) as {
|
||||
symbolProfileId: string;
|
||||
}[]) ?? []
|
||||
((await this.propertyService.getByKey(
|
||||
PROPERTY_BENCHMARKS
|
||||
)) as BenchmarkProperty[]) ?? []
|
||||
).map(({ symbolProfileId }) => {
|
||||
return symbolProfileId;
|
||||
});
|
||||
@ -204,6 +208,43 @@ export class BenchmarkService {
|
||||
return response;
|
||||
}
|
||||
|
||||
public async addBenchmark({
|
||||
dataSource,
|
||||
symbol
|
||||
}: UniqueAsset): Promise<Partial<SymbolProfile>> {
|
||||
const assetProfile = await this.prismaService.symbolProfile.findFirst({
|
||||
where: {
|
||||
dataSource,
|
||||
symbol
|
||||
}
|
||||
});
|
||||
|
||||
if (!assetProfile) {
|
||||
return;
|
||||
}
|
||||
|
||||
let benchmarks =
|
||||
((await this.propertyService.getByKey(
|
||||
PROPERTY_BENCHMARKS
|
||||
)) as BenchmarkProperty[]) ?? [];
|
||||
|
||||
benchmarks.push({ symbolProfileId: assetProfile.id });
|
||||
|
||||
benchmarks = uniqBy(benchmarks, 'symbolProfileId');
|
||||
|
||||
await this.propertyService.put({
|
||||
key: PROPERTY_BENCHMARKS,
|
||||
value: JSON.stringify(benchmarks)
|
||||
});
|
||||
|
||||
return {
|
||||
dataSource,
|
||||
symbol,
|
||||
id: assetProfile.id,
|
||||
name: assetProfile.name
|
||||
};
|
||||
}
|
||||
|
||||
private getMarketCondition(aPerformanceInPercent: number) {
|
||||
return aPerformanceInPercent <= -0.2 ? 'BEAR_MARKET' : 'NEUTRAL_MARKET';
|
||||
}
|
||||
|
@ -94,6 +94,13 @@ export class FrontendMiddleware implements NestMiddleware {
|
||||
) {
|
||||
featureGraphicPath = 'assets/images/blog/1000-stars-on-github.jpg';
|
||||
title = `Ghostfolio reaches 1’000 Stars on GitHub - ${title}`;
|
||||
} else if (
|
||||
request.path.startsWith(
|
||||
'/en/blog/2023/05/unlock-your-financial-potential-with-ghostfolio'
|
||||
)
|
||||
) {
|
||||
featureGraphicPath = 'assets/images/blog/20230520.jpg';
|
||||
title = `Unlock your Financial Potential with Ghostfolio - ${title}`;
|
||||
}
|
||||
|
||||
if (
|
||||
|
@ -35,6 +35,8 @@ export class ImportController {
|
||||
|
||||
@Post()
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||
public async import(
|
||||
@Body() importData: ImportDataDto,
|
||||
@Query('dryRun') isDryRun?: boolean
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
|
||||
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||
import {
|
||||
Activity,
|
||||
ActivityError
|
||||
} from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
||||
import { PlatformService } from '@ghostfolio/api/app/platform/platform.service';
|
||||
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
|
||||
@ -71,8 +74,25 @@ export class ImportService {
|
||||
|
||||
const value = new Big(quantity).mul(marketPrice).toNumber();
|
||||
|
||||
const isDuplicate = orders.some((activity) => {
|
||||
return (
|
||||
activity.SymbolProfile.currency === assetProfile.currency &&
|
||||
activity.SymbolProfile.dataSource === assetProfile.dataSource &&
|
||||
isSameDay(activity.date, parseDate(dateString)) &&
|
||||
activity.quantity === quantity &&
|
||||
activity.SymbolProfile.symbol === assetProfile.symbol &&
|
||||
activity.type === 'DIVIDEND' &&
|
||||
activity.unitPrice === marketPrice
|
||||
);
|
||||
});
|
||||
|
||||
const error: ActivityError = isDuplicate
|
||||
? { code: 'IS_DUPLICATE' }
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
Account,
|
||||
error,
|
||||
quantity,
|
||||
value,
|
||||
accountId: Account?.id,
|
||||
@ -84,7 +104,6 @@ export class ImportService {
|
||||
feeInBaseCurrency: 0,
|
||||
id: assetProfile.id,
|
||||
isDraft: false,
|
||||
isDuplicate: false, // TODO: Use evaluated state
|
||||
SymbolProfile: <SymbolProfile>(<unknown>assetProfile),
|
||||
symbolProfileId: assetProfile.id,
|
||||
type: 'DIVIDEND',
|
||||
@ -205,7 +224,7 @@ export class ImportService {
|
||||
userId
|
||||
});
|
||||
|
||||
const activitiesMarkedAsDuplicates = await this.markActivitiesAsDuplicates({
|
||||
const activitiesExtendedWithErrors = await this.extendActivitiesWithErrors({
|
||||
activitiesDto,
|
||||
userId
|
||||
});
|
||||
@ -228,13 +247,13 @@ export class ImportService {
|
||||
accountId,
|
||||
comment,
|
||||
date,
|
||||
error,
|
||||
fee,
|
||||
isDuplicate,
|
||||
quantity,
|
||||
SymbolProfile: assetProfile,
|
||||
type,
|
||||
unitPrice
|
||||
} of activitiesMarkedAsDuplicates) {
|
||||
} of activitiesExtendedWithErrors) {
|
||||
const validatedAccount = accounts.find(({ id }) => {
|
||||
return id === accountId;
|
||||
});
|
||||
@ -283,7 +302,7 @@ export class ImportService {
|
||||
updatedAt: new Date()
|
||||
};
|
||||
} else {
|
||||
if (isDuplicate) {
|
||||
if (error) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -321,7 +340,7 @@ export class ImportService {
|
||||
//@ts-ignore
|
||||
activities.push({
|
||||
...order,
|
||||
isDuplicate,
|
||||
error,
|
||||
value,
|
||||
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
||||
fee,
|
||||
@ -339,17 +358,7 @@ export class ImportService {
|
||||
return activities;
|
||||
}
|
||||
|
||||
private isUniqueAccount(accounts: AccountWithPlatform[]) {
|
||||
const uniqueAccountIds = new Set<string>();
|
||||
|
||||
for (const account of accounts) {
|
||||
uniqueAccountIds.add(account.id);
|
||||
}
|
||||
|
||||
return uniqueAccountIds.size === 1;
|
||||
}
|
||||
|
||||
private async markActivitiesAsDuplicates({
|
||||
private async extendActivitiesWithErrors({
|
||||
activitiesDto,
|
||||
userId
|
||||
}: {
|
||||
@ -389,12 +398,16 @@ export class ImportService {
|
||||
);
|
||||
});
|
||||
|
||||
const error: ActivityError = isDuplicate
|
||||
? { code: 'IS_DUPLICATE' }
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
accountId,
|
||||
comment,
|
||||
date,
|
||||
error,
|
||||
fee,
|
||||
isDuplicate,
|
||||
quantity,
|
||||
type,
|
||||
unitPrice,
|
||||
@ -421,6 +434,16 @@ export class ImportService {
|
||||
);
|
||||
}
|
||||
|
||||
private isUniqueAccount(accounts: AccountWithPlatform[]) {
|
||||
const uniqueAccountIds = new Set<string>();
|
||||
|
||||
for (const account of accounts) {
|
||||
uniqueAccountIds.add(account.id);
|
||||
}
|
||||
|
||||
return uniqueAccountIds.size === 1;
|
||||
}
|
||||
|
||||
private async validateActivities({
|
||||
activitiesDto,
|
||||
maxActivitiesToImport,
|
||||
|
@ -7,6 +7,7 @@ import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||
import { TagService } from '@ghostfolio/api/services/tag/tag.service';
|
||||
import {
|
||||
PROPERTY_BETTER_UPTIME_MONITOR_ID,
|
||||
PROPERTY_COUNTRIES_OF_SUBSCRIBERS,
|
||||
PROPERTY_DEMO_USER_ID,
|
||||
PROPERTY_IS_READ_ONLY_MODE,
|
||||
@ -16,19 +17,22 @@ import {
|
||||
ghostfolioFearAndGreedIndexDataSource
|
||||
} from '@ghostfolio/common/config';
|
||||
import {
|
||||
DATE_FORMAT,
|
||||
encodeDataSource,
|
||||
extractNumberFromString
|
||||
} from '@ghostfolio/common/helper';
|
||||
import { InfoItem } from '@ghostfolio/common/interfaces';
|
||||
import { Statistics } from '@ghostfolio/common/interfaces/statistics.interface';
|
||||
import { Subscription } from '@ghostfolio/common/interfaces/subscription.interface';
|
||||
import {
|
||||
InfoItem,
|
||||
Statistics,
|
||||
Subscription
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { permissions } from '@ghostfolio/common/permissions';
|
||||
import { SubscriptionOffer } from '@ghostfolio/common/types';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import * as bent from 'bent';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { subDays } from 'date-fns';
|
||||
import { format, subDays } from 'date-fns';
|
||||
|
||||
@Injectable()
|
||||
export class InfoService {
|
||||
@ -115,19 +119,28 @@ export class InfoService {
|
||||
globalPermissions.push(permissions.createUserAccount);
|
||||
}
|
||||
|
||||
const [benchmarks, demoAuthToken, statistics, subscriptions, tags] =
|
||||
await Promise.all([
|
||||
this.benchmarkService.getBenchmarkAssetProfiles(),
|
||||
this.getDemoAuthToken(),
|
||||
this.getStatistics(),
|
||||
this.getSubscriptions(),
|
||||
this.tagService.get()
|
||||
]);
|
||||
|
||||
return {
|
||||
...info,
|
||||
benchmarks,
|
||||
demoAuthToken,
|
||||
globalPermissions,
|
||||
isReadOnlyMode,
|
||||
platforms,
|
||||
statistics,
|
||||
subscriptions,
|
||||
systemMessage,
|
||||
tags,
|
||||
baseCurrency: this.configurationService.get('BASE_CURRENCY'),
|
||||
benchmarks: await this.benchmarkService.getBenchmarkAssetProfiles(),
|
||||
currencies: this.exchangeRateDataService.getCurrencies(),
|
||||
demoAuthToken: await this.getDemoAuthToken(),
|
||||
statistics: await this.getStatistics(),
|
||||
subscriptions: await this.getSubscriptions(),
|
||||
tags: await this.tagService.get()
|
||||
currencies: this.exchangeRateDataService.getCurrencies()
|
||||
};
|
||||
}
|
||||
|
||||
@ -291,6 +304,7 @@ export class InfoService {
|
||||
const gitHubContributors = await this.countGitHubContributors();
|
||||
const gitHubStargazers = await this.countGitHubStargazers();
|
||||
const slackCommunityUsers = await this.countSlackCommunityUsers();
|
||||
const uptime = await this.getUptime();
|
||||
|
||||
statistics = {
|
||||
activeUsers1d,
|
||||
@ -299,7 +313,8 @@ export class InfoService {
|
||||
gitHubContributors,
|
||||
gitHubStargazers,
|
||||
newUsers30d,
|
||||
slackCommunityUsers
|
||||
slackCommunityUsers,
|
||||
uptime
|
||||
};
|
||||
|
||||
await this.redisCacheService.set(
|
||||
@ -323,4 +338,36 @@ export class InfoService {
|
||||
|
||||
return JSON.parse(stripeConfig.value);
|
||||
}
|
||||
|
||||
private async getUptime(): Promise<number> {
|
||||
{
|
||||
try {
|
||||
const monitorId = (await this.propertyService.getByKey(
|
||||
PROPERTY_BETTER_UPTIME_MONITOR_ID
|
||||
)) as string;
|
||||
|
||||
const get = bent(
|
||||
`https://betteruptime.com/api/v2/monitors/${monitorId}/sla?from=${format(
|
||||
subDays(new Date(), 90),
|
||||
DATE_FORMAT
|
||||
)}&to${format(new Date(), DATE_FORMAT)}`,
|
||||
'GET',
|
||||
'json',
|
||||
200,
|
||||
{
|
||||
Authorization: `Bearer ${this.configurationService.get(
|
||||
'BETTER_UPTIME_API_KEY'
|
||||
)}`
|
||||
}
|
||||
);
|
||||
|
||||
const { data } = await get();
|
||||
return data.attributes.availability / 100;
|
||||
} catch (error) {
|
||||
Logger.error(error, 'InfoService');
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -68,5 +68,5 @@ export class CreateOrderDto {
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
updateAccountBalance: boolean;
|
||||
updateAccountBalance?: boolean;
|
||||
}
|
||||
|
@ -5,9 +5,14 @@ export interface Activities {
|
||||
}
|
||||
|
||||
export interface Activity extends OrderWithAccount {
|
||||
error?: ActivityError;
|
||||
feeInBaseCurrency: number;
|
||||
isDuplicate: boolean;
|
||||
updateAccountBalance?: boolean;
|
||||
value: number;
|
||||
valueInBaseCurrency: number;
|
||||
}
|
||||
|
||||
export interface ActivityError {
|
||||
code: 'IS_DUPLICATE';
|
||||
message?: string;
|
||||
}
|
||||
|
@ -333,7 +333,6 @@ export class OrderService {
|
||||
order.SymbolProfile.currency,
|
||||
userCurrency
|
||||
),
|
||||
isDuplicate: false,
|
||||
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
||||
value,
|
||||
order.SymbolProfile.currency,
|
||||
|
@ -67,8 +67,4 @@ export class UpdateOrderDto {
|
||||
|
||||
@IsNumber()
|
||||
unitPrice: number;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
updateAccountBalance: boolean;
|
||||
}
|
||||
|
@ -794,16 +794,6 @@ export class PortfolioService {
|
||||
let maxPrice = Math.max(orders[0].unitPrice, marketPrice);
|
||||
let minPrice = Math.min(orders[0].unitPrice, marketPrice);
|
||||
|
||||
if (!historicalData?.[aSymbol]?.[firstBuyDate]) {
|
||||
// Add historical entry for buy date, if no historical data available
|
||||
historicalDataArray.push({
|
||||
averagePrice: orders[0].unitPrice,
|
||||
date: firstBuyDate,
|
||||
marketPrice: orders[0].unitPrice,
|
||||
quantity: orders[0].quantity
|
||||
});
|
||||
}
|
||||
|
||||
if (historicalData[aSymbol]) {
|
||||
let j = -1;
|
||||
for (const [date, { marketPrice }] of Object.entries(
|
||||
@ -815,11 +805,16 @@ export class PortfolioService {
|
||||
) {
|
||||
j++;
|
||||
}
|
||||
|
||||
let currentAveragePrice = 0;
|
||||
let currentQuantity = 0;
|
||||
|
||||
const currentSymbol = transactionPoints[j].items.find(
|
||||
(item) => item.symbol === aSymbol
|
||||
({ symbol }) => {
|
||||
return symbol === aSymbol;
|
||||
}
|
||||
);
|
||||
|
||||
if (currentSymbol) {
|
||||
currentAveragePrice = currentSymbol.quantity.eq(0)
|
||||
? 0
|
||||
@ -829,14 +824,25 @@ export class PortfolioService {
|
||||
|
||||
historicalDataArray.push({
|
||||
date,
|
||||
marketPrice,
|
||||
averagePrice: currentAveragePrice,
|
||||
marketPrice:
|
||||
historicalDataArray.length > 0
|
||||
? marketPrice
|
||||
: currentAveragePrice,
|
||||
quantity: currentQuantity
|
||||
});
|
||||
|
||||
maxPrice = Math.max(marketPrice ?? 0, maxPrice);
|
||||
minPrice = Math.min(marketPrice ?? Number.MAX_SAFE_INTEGER, minPrice);
|
||||
}
|
||||
} else {
|
||||
// Add historical entry for buy date, if no historical data available
|
||||
historicalDataArray.push({
|
||||
averagePrice: orders[0].unitPrice,
|
||||
date: firstBuyDate,
|
||||
marketPrice: orders[0].unitPrice,
|
||||
quantity: orders[0].quantity
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
DEFAULT_LANGUAGE_CODE,
|
||||
PROPERTY_STRIPE_CONFIG
|
||||
} from '@ghostfolio/common/config';
|
||||
import { Subscription as SubscriptionInterface } from '@ghostfolio/common/interfaces/subscription.interface';
|
||||
import { Subscription as SubscriptionInterface } from '@ghostfolio/common/interfaces';
|
||||
import { UserWithSettings } from '@ghostfolio/common/types';
|
||||
import { SubscriptionType } from '@ghostfolio/common/types/subscription-type.type';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
@ -304,21 +304,29 @@ export class UserService {
|
||||
}
|
||||
|
||||
public async deleteUser(where: Prisma.UserWhereUniqueInput): Promise<User> {
|
||||
await this.prismaService.access.deleteMany({
|
||||
where: { OR: [{ granteeUserId: where.id }, { userId: where.id }] }
|
||||
});
|
||||
try {
|
||||
await this.prismaService.access.deleteMany({
|
||||
where: { OR: [{ granteeUserId: where.id }, { userId: where.id }] }
|
||||
});
|
||||
} catch {}
|
||||
|
||||
await this.prismaService.account.deleteMany({
|
||||
where: { userId: where.id }
|
||||
});
|
||||
try {
|
||||
await this.prismaService.account.deleteMany({
|
||||
where: { userId: where.id }
|
||||
});
|
||||
} catch {}
|
||||
|
||||
await this.prismaService.analytics.delete({
|
||||
where: { userId: where.id }
|
||||
});
|
||||
try {
|
||||
await this.prismaService.analytics.delete({
|
||||
where: { userId: where.id }
|
||||
});
|
||||
} catch {}
|
||||
|
||||
await this.prismaService.order.deleteMany({
|
||||
where: { userId: where.id }
|
||||
});
|
||||
try {
|
||||
await this.prismaService.order.deleteMany({
|
||||
where: { userId: where.id }
|
||||
});
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
await this.prismaService.settings.delete({
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
Injectable,
|
||||
NestInterceptor
|
||||
} from '@nestjs/common';
|
||||
import { DataSource } from '@prisma/client';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Injectable()
|
||||
@ -24,11 +25,24 @@ export class TransformDataSourceInRequestInterceptor<T>
|
||||
const request = http.getRequest();
|
||||
|
||||
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
||||
if (request.body.dataSource) {
|
||||
if (request.body.activities) {
|
||||
request.body.activities = request.body.activities.map((activity) => {
|
||||
if (DataSource[activity.dataSource]) {
|
||||
return activity;
|
||||
} else {
|
||||
return {
|
||||
...activity,
|
||||
dataSource: decodeDataSource(activity.dataSource)
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (request.body.dataSource && !DataSource[request.body.dataSource]) {
|
||||
request.body.dataSource = decodeDataSource(request.body.dataSource);
|
||||
}
|
||||
|
||||
if (request.params.dataSource) {
|
||||
if (request.params.dataSource && !DataSource[request.params.dataSource]) {
|
||||
request.params.dataSource = decodeDataSource(request.params.dataSource);
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ export class ConfigurationService {
|
||||
choices: ['AUD', 'CAD', 'CNY', 'EUR', 'GBP', 'JPY', 'RUB', 'USD'],
|
||||
default: 'USD'
|
||||
}),
|
||||
BETTER_UPTIME_API_KEY: str({ default: '' }),
|
||||
CACHE_TTL: num({ default: 1 }),
|
||||
DATA_SOURCE_EXCHANGE_RATES: str({ default: DataSource.YAHOO }),
|
||||
DATA_SOURCE_IMPORT: str({ default: DataSource.YAHOO }),
|
||||
@ -29,6 +30,7 @@ export class ConfigurationService {
|
||||
ENABLE_FEATURE_SUBSCRIPTION: bool({ default: false }),
|
||||
ENABLE_FEATURE_SYSTEM_MESSAGE: bool({ default: false }),
|
||||
EOD_HISTORICAL_DATA_API_KEY: str({ default: '' }),
|
||||
FINANCIAL_MODELING_PREP_API_KEY: str({ default: '' }),
|
||||
GOOGLE_CLIENT_ID: str({ default: 'dummyClientId' }),
|
||||
GOOGLE_SECRET: str({ default: 'dummySecret' }),
|
||||
GOOGLE_SHEETS_ACCOUNT: str({ default: '' }),
|
||||
|
@ -3,6 +3,7 @@ import { CryptocurrencyModule } from '@ghostfolio/api/services/cryptocurrency/cr
|
||||
import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service';
|
||||
import { CoinGeckoService } from '@ghostfolio/api/services/data-provider/coingecko/coingecko.service';
|
||||
import { EodHistoricalDataService } from '@ghostfolio/api/services/data-provider/eod-historical-data/eod-historical-data.service';
|
||||
import { FinancialModelingPrepService } from '@ghostfolio/api/services/data-provider/financial-modeling-prep/financial-modeling-prep.service';
|
||||
import { GoogleSheetsService } from '@ghostfolio/api/services/data-provider/google-sheets/google-sheets.service';
|
||||
import { ManualService } from '@ghostfolio/api/services/data-provider/manual/manual.service';
|
||||
import { RapidApiService } from '@ghostfolio/api/services/data-provider/rapid-api/rapid-api.service';
|
||||
@ -32,6 +33,7 @@ import { DataProviderService } from './data-provider.service';
|
||||
CoinGeckoService,
|
||||
DataProviderService,
|
||||
EodHistoricalDataService,
|
||||
FinancialModelingPrepService,
|
||||
GoogleSheetsService,
|
||||
ManualService,
|
||||
RapidApiService,
|
||||
@ -41,6 +43,7 @@ import { DataProviderService } from './data-provider.service';
|
||||
AlphaVantageService,
|
||||
CoinGeckoService,
|
||||
EodHistoricalDataService,
|
||||
FinancialModelingPrepService,
|
||||
GoogleSheetsService,
|
||||
ManualService,
|
||||
RapidApiService,
|
||||
@ -51,6 +54,7 @@ import { DataProviderService } from './data-provider.service';
|
||||
alphaVantageService,
|
||||
coinGeckoService,
|
||||
eodHistoricalDataService,
|
||||
financialModelingPrepService,
|
||||
googleSheetsService,
|
||||
manualService,
|
||||
rapidApiService,
|
||||
@ -59,6 +63,7 @@ import { DataProviderService } from './data-provider.service';
|
||||
alphaVantageService,
|
||||
coinGeckoService,
|
||||
eodHistoricalDataService,
|
||||
financialModelingPrepService,
|
||||
googleSheetsService,
|
||||
manualService,
|
||||
rapidApiService,
|
||||
|
@ -11,8 +11,7 @@ import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||
import { PROPERTY_DATA_SOURCE_MAPPING } from '@ghostfolio/common/config';
|
||||
import { DATE_FORMAT, getStartOfUtcDate } from '@ghostfolio/common/helper';
|
||||
import { UserWithSettings } from '@ghostfolio/common/types';
|
||||
import { Granularity } from '@ghostfolio/common/types';
|
||||
import type { Granularity, UserWithSettings } from '@ghostfolio/common/types';
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { DataSource, MarketData, SymbolProfile } from '@prisma/client';
|
||||
import { format, isValid } from 'date-fns';
|
||||
@ -399,7 +398,10 @@ export class DataProviderService {
|
||||
}
|
||||
|
||||
private isPremiumDataSource(aDataSource: DataSource) {
|
||||
const premiumDataSources: DataSource[] = [DataSource.EOD_HISTORICAL_DATA];
|
||||
const premiumDataSources: DataSource[] = [
|
||||
DataSource.EOD_HISTORICAL_DATA,
|
||||
DataSource.FINANCIAL_MODELING_PREP
|
||||
];
|
||||
return premiumDataSources.includes(aDataSource);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,181 @@
|
||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||
import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
|
||||
import {
|
||||
IDataProviderHistoricalResponse,
|
||||
IDataProviderResponse
|
||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
||||
import { DataProviderInfo } from '@ghostfolio/common/interfaces';
|
||||
import { Granularity } from '@ghostfolio/common/types';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { DataSource, SymbolProfile } from '@prisma/client';
|
||||
import bent from 'bent';
|
||||
import { format, isAfter, isBefore, isSameDay } from 'date-fns';
|
||||
|
||||
@Injectable()
|
||||
export class FinancialModelingPrepService implements DataProviderInterface {
|
||||
private apiKey: string;
|
||||
private baseCurrency: string;
|
||||
private readonly URL = 'https://financialmodelingprep.com/api/v3';
|
||||
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService
|
||||
) {
|
||||
this.apiKey = this.configurationService.get(
|
||||
'FINANCIAL_MODELING_PREP_API_KEY'
|
||||
);
|
||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
||||
}
|
||||
|
||||
public canHandle(symbol: string) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public async getAssetProfile(
|
||||
aSymbol: string
|
||||
): Promise<Partial<SymbolProfile>> {
|
||||
return {
|
||||
dataSource: this.getName(),
|
||||
symbol: aSymbol
|
||||
};
|
||||
}
|
||||
|
||||
public async getDividends({
|
||||
from,
|
||||
granularity = 'day',
|
||||
symbol,
|
||||
to
|
||||
}: {
|
||||
from: Date;
|
||||
granularity: Granularity;
|
||||
symbol: string;
|
||||
to: Date;
|
||||
}) {
|
||||
return {};
|
||||
}
|
||||
|
||||
public async getHistorical(
|
||||
aSymbol: string,
|
||||
aGranularity: Granularity = 'day',
|
||||
from: Date,
|
||||
to: Date
|
||||
): Promise<{
|
||||
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
||||
}> {
|
||||
try {
|
||||
const get = bent(
|
||||
`${this.URL}/historical-price-full/${aSymbol}?apikey=${this.apiKey}`,
|
||||
'GET',
|
||||
'json',
|
||||
200
|
||||
);
|
||||
const { historical } = await get();
|
||||
|
||||
const result: {
|
||||
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
||||
} = {
|
||||
[aSymbol]: {}
|
||||
};
|
||||
|
||||
for (const { close, date } of historical) {
|
||||
if (
|
||||
(isSameDay(parseDate(date), from) ||
|
||||
isAfter(parseDate(date), from)) &&
|
||||
isBefore(parseDate(date), to)
|
||||
) {
|
||||
result[aSymbol][date] = {
|
||||
marketPrice: close
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Could not get historical market data for ${aSymbol} (${this.getName()}) from ${format(
|
||||
from,
|
||||
DATE_FORMAT
|
||||
)} to ${format(to, DATE_FORMAT)}: [${error.name}] ${error.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public getName(): DataSource {
|
||||
return DataSource.FINANCIAL_MODELING_PREP;
|
||||
}
|
||||
|
||||
public async getQuotes(
|
||||
aSymbols: string[]
|
||||
): Promise<{ [symbol: string]: IDataProviderResponse }> {
|
||||
const results: { [symbol: string]: IDataProviderResponse } = {};
|
||||
|
||||
if (aSymbols.length <= 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
const get = bent(
|
||||
`${this.URL}/quote/${aSymbols.join(',')}?apikey=${this.apiKey}`,
|
||||
'GET',
|
||||
'json',
|
||||
200
|
||||
);
|
||||
const response = await get();
|
||||
|
||||
for (const { price, symbol } of response) {
|
||||
results[symbol] = {
|
||||
currency: this.baseCurrency,
|
||||
dataProviderInfo: this.getDataProviderInfo(),
|
||||
dataSource: DataSource.FINANCIAL_MODELING_PREP,
|
||||
marketPrice: price,
|
||||
marketState: 'delayed'
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(error, 'FinancialModelingPrepService');
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public getTestSymbol() {
|
||||
return 'AAPL';
|
||||
}
|
||||
|
||||
public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
|
||||
let items: LookupItem[] = [];
|
||||
|
||||
try {
|
||||
const get = bent(
|
||||
`${this.URL}/search?query=${aQuery}&apikey=${this.apiKey}`,
|
||||
'GET',
|
||||
'json',
|
||||
200
|
||||
);
|
||||
const result = await get();
|
||||
|
||||
items = result.map(({ currency, name, symbol }) => {
|
||||
return {
|
||||
// TODO: Add assetClass
|
||||
// TODO: Add assetSubClass
|
||||
currency,
|
||||
name,
|
||||
symbol,
|
||||
dataSource: this.getName()
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
Logger.error(error, 'FinancialModelingPrepService');
|
||||
}
|
||||
|
||||
return { items };
|
||||
}
|
||||
|
||||
private getDataProviderInfo(): DataProviderInfo {
|
||||
return {
|
||||
name: 'Financial Modeling Prep',
|
||||
url: 'https://financialmodelingprep.com/developer/docs'
|
||||
};
|
||||
}
|
||||
}
|
@ -138,8 +138,7 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
marketPrice: this.getConvertedValue({
|
||||
symbol: aSymbol,
|
||||
value: historicalItem.close
|
||||
}),
|
||||
performance: historicalItem.open - historicalItem.close
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ export interface Environment extends CleanedEnvAccessors {
|
||||
ACCESS_TOKEN_SALT: string;
|
||||
ALPHA_VANTAGE_API_KEY: string;
|
||||
BASE_CURRENCY: string;
|
||||
BETTER_UPTIME_API_KEY: string;
|
||||
CACHE_TTL: number;
|
||||
DATA_SOURCE_EXCHANGE_RATES: string;
|
||||
DATA_SOURCE_IMPORT: string;
|
||||
@ -16,6 +17,7 @@ export interface Environment extends CleanedEnvAccessors {
|
||||
ENABLE_FEATURE_SUBSCRIPTION: boolean;
|
||||
ENABLE_FEATURE_SYSTEM_MESSAGE: boolean;
|
||||
EOD_HISTORICAL_DATA_API_KEY: string;
|
||||
FINANCIAL_MODELING_PREP_API_KEY: string;
|
||||
GOOGLE_CLIENT_ID: string;
|
||||
GOOGLE_SECRET: string;
|
||||
GOOGLE_SHEETS_ACCOUNT: string;
|
||||
|
@ -23,7 +23,6 @@ export interface IOrder {
|
||||
|
||||
export interface IDataProviderHistoricalResponse {
|
||||
marketPrice: number;
|
||||
performance?: number;
|
||||
}
|
||||
|
||||
export interface IDataProviderResponse {
|
||||
|
@ -5,25 +5,46 @@ import { PageTitleStrategy } from '@ghostfolio/client/services/page-title.strate
|
||||
import { ModulePreloadService } from './core/module-preload.service';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: 'about',
|
||||
...[
|
||||
'about',
|
||||
/////
|
||||
'a-propos',
|
||||
'informazioni-su',
|
||||
'over',
|
||||
'ueber-uns'
|
||||
].map((path) => ({
|
||||
path,
|
||||
loadChildren: () =>
|
||||
import('./pages/about/about-page.module').then((m) => m.AboutPageModule)
|
||||
},
|
||||
{
|
||||
path: 'about/changelog',
|
||||
})),
|
||||
...[
|
||||
'about/changelog',
|
||||
/////
|
||||
'a-propos/changelog',
|
||||
'informazioni-su/changelog',
|
||||
'over/changelog',
|
||||
'ueber-uns/changelog'
|
||||
].map((path) => ({
|
||||
path,
|
||||
loadChildren: () =>
|
||||
import('./pages/about/changelog/changelog-page.module').then(
|
||||
(m) => m.ChangelogPageModule
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'about/privacy-policy',
|
||||
})),
|
||||
...[
|
||||
'about/privacy-policy',
|
||||
/////
|
||||
'a-propos/politique-de-confidentialite',
|
||||
'informazioni-su/informativa-sulla-privacy',
|
||||
'over/privacybeleid',
|
||||
'ueber-uns/datenschutzbestimmungen'
|
||||
].map((path) => ({
|
||||
path,
|
||||
loadChildren: () =>
|
||||
import('./pages/about/privacy-policy/privacy-policy-page.module').then(
|
||||
(m) => m.PrivacyPolicyPageModule
|
||||
)
|
||||
},
|
||||
})),
|
||||
{
|
||||
path: 'account',
|
||||
loadChildren: () =>
|
||||
@ -48,11 +69,11 @@ const routes: Routes = [
|
||||
loadChildren: () =>
|
||||
import('./pages/auth/auth-page.module').then((m) => m.AuthPageModule)
|
||||
},
|
||||
{
|
||||
path: 'blog',
|
||||
...['blog'].map((path) => ({
|
||||
path,
|
||||
loadChildren: () =>
|
||||
import('./pages/blog/blog-page.module').then((m) => m.BlogPageModule)
|
||||
},
|
||||
})),
|
||||
{
|
||||
path: 'blog/2021/07/hallo-ghostfolio',
|
||||
loadChildren: () =>
|
||||
@ -137,34 +158,66 @@ const routes: Routes = [
|
||||
'./pages/blog/2023/03/1000-stars-on-github/1000-stars-on-github-page.module'
|
||||
).then((m) => m.ThousandStarsOnGitHubPageModule)
|
||||
},
|
||||
{
|
||||
path: 'blog/2023/05/unlock-your-financial-potential-with-ghostfolio',
|
||||
loadChildren: () =>
|
||||
import(
|
||||
'./pages/blog/2023/05/unlock-your-financial-potential-with-ghostfolio/unlock-your-financial-potential-with-ghostfolio-page.module'
|
||||
).then((m) => m.UnlockYourFinancialPotentialWithGhostfolioPageModule)
|
||||
},
|
||||
{
|
||||
path: 'demo',
|
||||
loadChildren: () =>
|
||||
import('./pages/demo/demo-page.module').then((m) => m.DemoPageModule)
|
||||
},
|
||||
{
|
||||
path: 'faq',
|
||||
...[
|
||||
'faq',
|
||||
/////
|
||||
'domande-piu-frequenti',
|
||||
'foire-aux-questions',
|
||||
'haeufig-gestellte-fragen',
|
||||
'vaak-gestelde-vragen'
|
||||
].map((path) => ({
|
||||
path,
|
||||
loadChildren: () =>
|
||||
import('./pages/faq/faq-page.module').then((m) => m.FaqPageModule)
|
||||
},
|
||||
{
|
||||
path: 'features',
|
||||
})),
|
||||
...[
|
||||
'features',
|
||||
/////
|
||||
'fonctionnalites',
|
||||
'funzionalita',
|
||||
'kenmerken'
|
||||
].map((path) => ({
|
||||
path,
|
||||
loadChildren: () =>
|
||||
import('./pages/features/features-page.module').then(
|
||||
(m) => m.FeaturesPageModule
|
||||
)
|
||||
},
|
||||
})),
|
||||
{
|
||||
path: 'home',
|
||||
loadChildren: () =>
|
||||
import('./pages/home/home-page.module').then((m) => m.HomePageModule)
|
||||
},
|
||||
{
|
||||
path: 'markets',
|
||||
...[
|
||||
'markets',
|
||||
/////
|
||||
'maerkte',
|
||||
'marches',
|
||||
'markten',
|
||||
'mercati'
|
||||
].map((path) => ({
|
||||
path,
|
||||
loadChildren: () =>
|
||||
import('./pages/markets/markets-page.module').then(
|
||||
(m) => m.MarketsPageModule
|
||||
)
|
||||
})),
|
||||
{
|
||||
path: 'open',
|
||||
loadChildren: () =>
|
||||
import('./pages/open/open-page.module').then((m) => m.OpenPageModule)
|
||||
},
|
||||
{
|
||||
path: 'p',
|
||||
@ -180,27 +233,48 @@ const routes: Routes = [
|
||||
(m) => m.PortfolioPageModule
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'pricing',
|
||||
...[
|
||||
'pricing',
|
||||
/////
|
||||
'preise',
|
||||
'prezzi',
|
||||
'prijzen',
|
||||
'prix'
|
||||
].map((path) => ({
|
||||
path,
|
||||
loadChildren: () =>
|
||||
import('./pages/pricing/pricing-page.module').then(
|
||||
(m) => m.PricingPageModule
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'register',
|
||||
})),
|
||||
...[
|
||||
'register',
|
||||
/////
|
||||
'enregistrement',
|
||||
'iscrizione',
|
||||
'registratie',
|
||||
'registrierung'
|
||||
].map((path) => ({
|
||||
path,
|
||||
loadChildren: () =>
|
||||
import('./pages/register/register-page.module').then(
|
||||
(m) => m.RegisterPageModule
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'resources',
|
||||
})),
|
||||
...[
|
||||
'resources',
|
||||
/////
|
||||
'bronnen',
|
||||
'ressourcen',
|
||||
'ressources',
|
||||
'risorse'
|
||||
].map((path) => ({
|
||||
path,
|
||||
loadChildren: () =>
|
||||
import('./pages/resources/resources-page.module').then(
|
||||
(m) => m.ResourcesPageModule
|
||||
)
|
||||
},
|
||||
})),
|
||||
{
|
||||
path: 'start',
|
||||
loadChildren: () =>
|
||||
|
@ -13,7 +13,9 @@ import { downloadAsFile } from '@ghostfolio/common/helper';
|
||||
import { User } from '@ghostfolio/common/interfaces';
|
||||
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||
import { translate } from '@ghostfolio/ui/i18n';
|
||||
import Big from 'big.js';
|
||||
import { format, parseISO } from 'date-fns';
|
||||
import { isNumber } from 'lodash';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@ -28,6 +30,9 @@ import { AccountDetailDialogParams } from './interfaces/interfaces';
|
||||
})
|
||||
export class AccountDetailDialog implements OnDestroy, OnInit {
|
||||
public accountType: string;
|
||||
public balance: number;
|
||||
public currency: string;
|
||||
public equity: number;
|
||||
public name: string;
|
||||
public orders: OrderWithAccount[];
|
||||
public platformName: string;
|
||||
@ -58,14 +63,33 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
||||
this.dataService
|
||||
.fetchAccount(this.data.accountId)
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ accountType, name, Platform, valueInBaseCurrency }) => {
|
||||
this.accountType = translate(accountType);
|
||||
this.name = name;
|
||||
this.platformName = Platform?.name ?? '-';
|
||||
this.valueInBaseCurrency = valueInBaseCurrency;
|
||||
.subscribe(
|
||||
({
|
||||
accountType,
|
||||
balance,
|
||||
currency,
|
||||
name,
|
||||
Platform,
|
||||
value,
|
||||
valueInBaseCurrency
|
||||
}) => {
|
||||
this.accountType = translate(accountType);
|
||||
this.balance = balance;
|
||||
this.currency = currency;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
if (isNumber(balance) && isNumber(value)) {
|
||||
this.equity = new Big(value).minus(balance).toNumber();
|
||||
} else {
|
||||
this.equity = null;
|
||||
}
|
||||
|
||||
this.name = name;
|
||||
this.platformName = Platform?.name ?? '-';
|
||||
this.valueInBaseCurrency = valueInBaseCurrency;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
);
|
||||
|
||||
this.dataService
|
||||
.fetchActivities({
|
||||
|
@ -20,6 +20,26 @@
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6 mb-3">
|
||||
<gf-value
|
||||
i18n
|
||||
size="medium"
|
||||
[currency]="currency"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="balance"
|
||||
>Cash Balance</gf-value
|
||||
>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<gf-value
|
||||
i18n
|
||||
size="medium"
|
||||
[currency]="currency"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="equity"
|
||||
>Equity</gf-value
|
||||
>
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<gf-value i18n size="medium" [value]="accountType"
|
||||
>Account Type</gf-value
|
||||
|
@ -143,18 +143,6 @@
|
||||
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||
</button>
|
||||
<mat-menu #assetProfileActionsMenu="matMenu" xPosition="before">
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="onGatherSymbol({dataSource: element.dataSource, symbol: element.symbol})"
|
||||
>
|
||||
<ng-container i18n>Gather Historical Data</ng-container>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="onGatherProfileDataBySymbol({dataSource: element.dataSource, symbol: element.symbol})"
|
||||
>
|
||||
<ng-container i18n>Gather Profile Data</ng-container>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
[disabled]="element.activitiesCount !== 0"
|
||||
|
@ -10,13 +10,13 @@ import { FormBuilder } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto';
|
||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import {
|
||||
AdminMarketDataDetails,
|
||||
EnhancedSymbolProfile,
|
||||
UniqueAsset
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { translate } from '@ghostfolio/ui/i18n';
|
||||
import { MarketData } from '@prisma/client';
|
||||
import { MarketData, SymbolProfile } from '@prisma/client';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@ -37,9 +37,11 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
||||
symbolMapping: ''
|
||||
});
|
||||
public assetSubClass: string;
|
||||
public benchmarks: Partial<SymbolProfile>[];
|
||||
public countries: {
|
||||
[code: string]: { name: string; value: number };
|
||||
};
|
||||
public isBenchmark = false;
|
||||
public marketDataDetails: MarketData[] = [];
|
||||
public sectors: {
|
||||
[name: string]: { name: string; value: number };
|
||||
@ -51,11 +53,14 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
||||
private adminService: AdminService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
@Inject(MAT_DIALOG_DATA) public data: AssetProfileDialogParams,
|
||||
private dataService: DataService,
|
||||
public dialogRef: MatDialogRef<AssetProfileDialog>,
|
||||
private formBuilder: FormBuilder
|
||||
) {}
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.benchmarks = this.dataService.fetchInfo().benchmarks;
|
||||
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
@ -72,6 +77,9 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
||||
this.assetClass = translate(this.assetProfile?.assetClass);
|
||||
this.assetSubClass = translate(this.assetProfile?.assetSubClass);
|
||||
this.countries = {};
|
||||
this.isBenchmark = this.benchmarks.some(({ id }) => {
|
||||
return id === this.assetProfile.id;
|
||||
});
|
||||
this.marketDataDetails = marketData;
|
||||
this.sectors = {};
|
||||
|
||||
@ -128,6 +136,17 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
public onSetBenchmark({ dataSource, symbol }: UniqueAsset) {
|
||||
this.dataService
|
||||
.postBenchmark({ dataSource, symbol })
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 300);
|
||||
});
|
||||
}
|
||||
|
||||
public onSubmit() {
|
||||
let symbolMapping = {};
|
||||
|
||||
|
@ -37,6 +37,13 @@
|
||||
>
|
||||
<ng-container i18n>Gather Profile Data</ng-container>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
[disabled]="isBenchmark"
|
||||
(click)="onSetBenchmark({dataSource: data.dataSource, symbol: data.symbol})"
|
||||
>
|
||||
<ng-container i18n>Set as Benchmark</ng-container>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
|
||||
|
@ -72,19 +72,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
*ngIf="info?.benchmarks?.length > 0"
|
||||
class="align-items-start d-flex my-3"
|
||||
>
|
||||
<div class="w-50" i18n>Benchmarks</div>
|
||||
<div class="w-50">
|
||||
<table>
|
||||
<tr *ngFor="let benchmark of info.benchmarks">
|
||||
<td class="pl-1">{{ benchmark.symbol }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
*ngIf="info?.tags?.length > 0"
|
||||
class="align-items-start d-flex my-3"
|
||||
|
@ -1,6 +1,17 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="d-flex justify-content-end">
|
||||
<a
|
||||
color="primary"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[queryParams]="{ createPlatformDialog: true }"
|
||||
[routerLink]="[]"
|
||||
>
|
||||
Add Platform
|
||||
</a>
|
||||
</div>
|
||||
<table
|
||||
class="gf-table w-100"
|
||||
mat-table
|
||||
@ -91,16 +102,4 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="hasPermissionToCreatePlatform" 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"></ion-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -2,11 +2,4 @@
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
.fab-container {
|
||||
position: fixed;
|
||||
right: 2rem;
|
||||
bottom: 4rem;
|
||||
z-index: 999;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
OnDestroy,
|
||||
@ -13,8 +14,7 @@ import { CreatePlatformDto } from '@ghostfolio/api/app/platform/create-platform.
|
||||
import { UpdatePlatformDto } from '@ghostfolio/api/app/platform/update-platform.dto';
|
||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { Platform, Platform as PlatformModel } from '@prisma/client';
|
||||
import { Platform } from '@prisma/client';
|
||||
import { get } from 'lodash';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
@ -22,7 +22,7 @@ import { Subject, takeUntil } from 'rxjs';
|
||||
import { CreateOrUpdatePlatformDialog } from './create-or-update-platform-dialog/create-or-update-account-platform.component';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 'gf-admin-platform',
|
||||
styleUrls: ['./admin-platform.component.scss'],
|
||||
templateUrl: './admin-platform.component.html'
|
||||
@ -33,9 +33,7 @@ export class AdminPlatformComponent implements OnInit, OnDestroy {
|
||||
public dataSource: MatTableDataSource<Platform> = new MatTableDataSource();
|
||||
public deviceType: string;
|
||||
public displayedColumns = ['name', 'url', 'accounts', 'actions'];
|
||||
public hasPermissionToCreatePlatform: boolean;
|
||||
public hasPermissionToDeletePlatform: boolean;
|
||||
public platforms: PlatformModel[];
|
||||
public platforms: Platform[];
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
@ -51,9 +49,9 @@ export class AdminPlatformComponent implements OnInit, OnDestroy {
|
||||
this.route.queryParams
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((params) => {
|
||||
if (params['createDialog'] && this.hasPermissionToCreatePlatform) {
|
||||
if (params['createPlatformDialog']) {
|
||||
this.openCreatePlatformDialog();
|
||||
} else if (params['editDialog']) {
|
||||
} else if (params['editPlatformDialog']) {
|
||||
if (this.platforms) {
|
||||
const platform = this.platforms.find(({ id }) => {
|
||||
return id === params['platformId'];
|
||||
@ -70,25 +68,6 @@ export class AdminPlatformComponent implements OnInit, OnDestroy {
|
||||
public ngOnInit() {
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((state) => {
|
||||
if (state?.user) {
|
||||
const user = state.user;
|
||||
|
||||
this.hasPermissionToCreatePlatform = hasPermission(
|
||||
user.permissions,
|
||||
permissions.createPlatform
|
||||
);
|
||||
this.hasPermissionToDeletePlatform = hasPermission(
|
||||
user.permissions,
|
||||
permissions.deletePlatform
|
||||
);
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
|
||||
this.fetchPlatforms();
|
||||
}
|
||||
|
||||
@ -102,9 +81,9 @@ export class AdminPlatformComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
public onUpdatePlatform(aPlatform: PlatformModel) {
|
||||
public onUpdatePlatform({ id }: Platform) {
|
||||
this.router.navigate([], {
|
||||
queryParams: { platformId: aPlatform.id, editDialog: true }
|
||||
queryParams: { editPlatformDialog: true, platformId: id }
|
||||
});
|
||||
}
|
||||
|
@ -7,11 +7,12 @@ import { MatTableModule } from '@angular/material/table';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module';
|
||||
|
||||
import { AdminPlatformComponent } from './admin-platform.component';
|
||||
import { GfCreateOrUpdatePlatformDialogModule } from './create-or-update-platform-dialog/create-or-update-platform-dialog.module';
|
||||
import { AdminPlatformComponent } from './platform.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AdminPlatformComponent],
|
||||
exports: [AdminPlatformComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfCreateOrUpdatePlatformDialogModule,
|
||||
|
@ -5,9 +5,9 @@ import { Subject } from 'rxjs';
|
||||
import { CreateOrUpdatePlatformDialogParams } from './interfaces/interfaces';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'h-100' },
|
||||
selector: 'gf-create-or-update-platform-dialog',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
styleUrls: ['./create-or-update-platform-dialog.scss'],
|
||||
templateUrl: 'create-or-update-platform-dialog.html'
|
||||
})
|
||||
|
@ -0,0 +1,15 @@
|
||||
<div class="container">
|
||||
<div class="mb-5 row">
|
||||
<div class="col">
|
||||
<h3 class="text-center" i18n>Platforms</h3>
|
||||
<gf-admin-platform></gf-admin-platform>
|
||||
</div>
|
||||
</div>
|
||||
<!--
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3 class="text-center" i18n>Tags</h3>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
@ -0,0 +1,5 @@
|
||||
@import 'apps/client/src/styles/ghostfolio-style';
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'page' },
|
||||
selector: 'gf-admin-settings',
|
||||
styleUrls: ['./admin-settings.component.scss'],
|
||||
templateUrl: './admin-settings.component.html'
|
||||
})
|
||||
export class AdminSettingsComponent implements OnInit, OnDestroy {
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor() {}
|
||||
|
||||
public ngOnInit() {}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { GfAdminPlatformModule } from '@ghostfolio/client/components/admin-platform/admin-platform.module';
|
||||
|
||||
import { AdminSettingsComponent } from './admin-settings.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AdminSettingsComponent],
|
||||
imports: [CommonModule, GfAdminPlatformModule, RouterModule],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfAdminSettingsModule {}
|
@ -13,7 +13,6 @@
|
||||
appearance="outline"
|
||||
class="w-100 without-hint"
|
||||
color="accent"
|
||||
[hidden]="benchmarks?.length === 0"
|
||||
>
|
||||
<mat-label i18n>Compare with...</mat-label>
|
||||
<mat-select
|
||||
@ -28,6 +27,12 @@
|
||||
[value]="symbolProfile.id"
|
||||
>{{ symbolProfile.name }}</mat-option
|
||||
>
|
||||
<mat-option
|
||||
*ngIf="hasPermissionToAccessAdminControl"
|
||||
i18n
|
||||
[routerLink]="['/admin', 'market-data']"
|
||||
>Manage Benchmarks</mat-option
|
||||
>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
@ -23,6 +23,7 @@ import {
|
||||
parseDate
|
||||
} from '@ghostfolio/common/helper';
|
||||
import { LineChartItem, User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { ColorScheme } from '@ghostfolio/common/types';
|
||||
import { SymbolProfile } from '@prisma/client';
|
||||
import {
|
||||
@ -59,6 +60,7 @@ export class BenchmarkComparatorComponent implements OnChanges, OnDestroy {
|
||||
@ViewChild('chartCanvas') chartCanvas;
|
||||
|
||||
public chart: Chart<'line'>;
|
||||
public hasPermissionToAccessAdminControl: boolean;
|
||||
|
||||
public constructor() {
|
||||
Chart.register(
|
||||
@ -76,6 +78,11 @@ export class BenchmarkComparatorComponent implements OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
public ngOnChanges() {
|
||||
this.hasPermissionToAccessAdminControl = hasPermission(
|
||||
this.user?.permissions,
|
||||
permissions.accessAdminControl
|
||||
);
|
||||
|
||||
if (this.performanceDataItems) {
|
||||
this.initialize();
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
|
||||
@ -16,7 +17,8 @@ import { BenchmarkComparatorComponent } from './benchmark-comparator.component';
|
||||
GfPremiumIndicatorModule,
|
||||
MatSelectModule,
|
||||
NgxSkeletonLoaderModule,
|
||||
ReactiveFormsModule
|
||||
ReactiveFormsModule,
|
||||
RouterModule
|
||||
]
|
||||
})
|
||||
export class GfBenchmarkComparatorModule {}
|
||||
|
@ -1,16 +1,16 @@
|
||||
<div class="container p-0">
|
||||
<div class="row px-3 py-1">
|
||||
<div class="d-flex flex-grow-1" i18n>Time in Market</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
<div class="flex-grow-1 text-truncate" i18n>Time in Market</div>
|
||||
<div class="justify-content-end">
|
||||
<gf-value class="justify-content-end" [value]="timeInMarket"></gf-value>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col"><hr /></div>
|
||||
</div>
|
||||
<div class="row px-3 py-1">
|
||||
<div class="d-flex flex-grow-1" i18n>Buy</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
<div class="flex-grow-1 text-truncate" i18n>Buy</div>
|
||||
<div class="justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
[currency]="baseCurrency"
|
||||
@ -19,9 +19,9 @@
|
||||
></gf-value>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row px-3 py-1">
|
||||
<div class="d-flex flex-grow-1" i18n>Sell</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
<div class="flex-grow-1 text-truncate" i18n>Sell</div>
|
||||
<div class="justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
[currency]="baseCurrency"
|
||||
@ -33,9 +33,9 @@
|
||||
<div class="row">
|
||||
<div class="col"><hr /></div>
|
||||
</div>
|
||||
<div class="row px-3 py-1">
|
||||
<div class="d-flex flex-grow-1" i18n>Investment</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
<div class="flex-grow-1 text-truncate" i18n>Investment</div>
|
||||
<div class="justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
[currency]="baseCurrency"
|
||||
@ -44,9 +44,9 @@
|
||||
></gf-value>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row px-3 py-1">
|
||||
<div class="d-flex flex-grow-1" i18n>Absolute Gross Performance</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
<div class="flex-grow-1 text-truncate" i18n>Absolute Gross Performance</div>
|
||||
<div class="justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
[currency]="baseCurrency"
|
||||
@ -55,9 +55,9 @@
|
||||
></gf-value>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row px-3 py-1">
|
||||
<div class="d-flex flex-grow-1 ml-3" i18n>Gross Performance (TWR)</div>
|
||||
<div class="d-flex flex-column flex-wrap justify-content-end">
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
<div class="flex-grow-1 ml-3 text-truncate" i18n>Gross Performance</div>
|
||||
<div class="flex-column flex-wrap justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
position="end"
|
||||
@ -70,8 +70,8 @@
|
||||
></gf-value>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row px-3 py-1">
|
||||
<div class="d-flex flex-grow-1" i18n>
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
<div class="flex-grow-1 text-truncate" i18n>
|
||||
Fees for {{ summary?.ordersCount }} {summary?.ordersCount, plural, =1
|
||||
{transaction} other {transactions}}
|
||||
</div>
|
||||
@ -88,9 +88,9 @@
|
||||
<div class="row">
|
||||
<div class="col"><hr /></div>
|
||||
</div>
|
||||
<div class="row px-3 py-1">
|
||||
<div class="d-flex flex-grow-1" i18n>Absolute Net Performance</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
<div class="flex-grow-1 text-truncate" i18n>Absolute Net Performance</div>
|
||||
<div class="justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
[currency]="baseCurrency"
|
||||
@ -99,9 +99,9 @@
|
||||
></gf-value>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row px-3 py-1">
|
||||
<div class="d-flex flex-grow-1 ml-3" i18n>Net Performance (TWR)</div>
|
||||
<div class="d-flex flex-column flex-wrap justify-content-end">
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
<div class="flex-grow-1 text-truncate ml-3" i18n>Net Performance</div>
|
||||
<div class="flex-column flex-wrap justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
position="end"
|
||||
@ -115,9 +115,9 @@
|
||||
<div class="row">
|
||||
<div class="col"><hr /></div>
|
||||
</div>
|
||||
<div class="row px-3 py-1">
|
||||
<div class="d-flex flex-grow-1" i18n>Total Assets</div>
|
||||
<div class="d-flex flex-column flex-wrap justify-content-end">
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
<div class="flex-grow-1 text-truncate" i18n>Total Assets</div>
|
||||
<div class="flex-column flex-wrap justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
position="end"
|
||||
@ -127,9 +127,9 @@
|
||||
></gf-value>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row px-3 py-1">
|
||||
<div class="d-flex flex-grow-1" i18n>Valuables</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
<div class="flex-grow-1 text-truncate" i18n>Valuables</div>
|
||||
<div class="justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
[currency]="baseCurrency"
|
||||
@ -138,8 +138,8 @@
|
||||
></gf-value>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row px-3 py-1">
|
||||
<div class="d-flex flex-grow-1" i18n>Emergency Fund</div>
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
<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 }"
|
||||
@ -158,9 +158,9 @@
|
||||
></gf-value>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row px-3 py-1">
|
||||
<div class="d-flex flex-grow-1" i18n>Buying Power</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
<div class="flex-grow-1 text-truncate" i18n>Buying Power</div>
|
||||
<div class="justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
[currency]="baseCurrency"
|
||||
@ -169,9 +169,9 @@
|
||||
></gf-value>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row px-3 py-1">
|
||||
<div class="d-flex flex-grow-1" i18n>Excluded from Analysis</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
<div class="flex-grow-1 text-truncate" i18n>Excluded from Analysis</div>
|
||||
<div class="justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
[currency]="baseCurrency"
|
||||
@ -183,9 +183,9 @@
|
||||
<div class="row">
|
||||
<div class="col"><hr /></div>
|
||||
</div>
|
||||
<div class="row px-3 py-1">
|
||||
<div class="d-flex flex-grow-1 font-weight-bold" i18n>Net Worth</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
<div class="flex-grow-1 font-weight-bold text-truncate" i18n>Net Worth</div>
|
||||
<div class="justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
[currency]="baseCurrency"
|
||||
@ -194,9 +194,11 @@
|
||||
></gf-value>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row px-3 py-1">
|
||||
<div class="d-flex flex-grow-1 ml-3" i18n>Annualized Performance</div>
|
||||
<div class="d-flex flex-column flex-wrap justify-content-end">
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
<div class="flex-grow-1 ml-3 text-truncate" i18n>
|
||||
Annualized Performance
|
||||
</div>
|
||||
<div class="flex-column flex-wrap justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
position="end"
|
||||
@ -210,9 +212,9 @@
|
||||
<div class="row">
|
||||
<div class="col"><hr /></div>
|
||||
</div>
|
||||
<div class="row px-3 py-1">
|
||||
<div class="d-flex flex-grow-1" i18n>Dividend</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
<div class="flex-grow-1 text-truncate" i18n>Dividend</div>
|
||||
<div class="justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
[currency]="baseCurrency"
|
||||
|
@ -1,10 +1,11 @@
|
||||
<mat-radio-group
|
||||
class="text-nowrap"
|
||||
class="d-block text-nowrap"
|
||||
[formControl]="option"
|
||||
(change)="onValueChange()"
|
||||
>
|
||||
<mat-radio-button
|
||||
*ngFor="let option of options"
|
||||
class="d-inline-flex"
|
||||
[disabled]="isLoading"
|
||||
[ngClass]="{ 'cursor-pointer': !isLoading }"
|
||||
[value]="option.value"
|
||||
|
@ -24,6 +24,7 @@ export class AuthGuard implements CanActivate {
|
||||
'/faq',
|
||||
'/features',
|
||||
'/markets',
|
||||
'/open',
|
||||
'/p',
|
||||
'/pricing',
|
||||
'/register',
|
||||
|
@ -1,14 +1,13 @@
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { environment } from '@ghostfolio/client/../environments/environment';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { User } from '@ghostfolio/common/interfaces';
|
||||
import { Statistics } from '@ghostfolio/common/interfaces/statistics.interface';
|
||||
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
|
||||
import { Statistics, User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { environment } from '../../../environments/environment';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
selector: 'gf-about-page',
|
||||
@ -16,6 +15,7 @@ import { environment } from '../../../environments/environment';
|
||||
templateUrl: './about-page.html'
|
||||
})
|
||||
export class AboutPageComponent implements OnDestroy, OnInit {
|
||||
public defaultLanguageCode = DEFAULT_LANGUAGE_CODE;
|
||||
public hasPermissionForBlog: boolean;
|
||||
public hasPermissionForStatistics: boolean;
|
||||
public hasPermissionForSubscription: boolean;
|
||||
|
@ -7,8 +7,25 @@
|
||||
Ghostfolio is a lightweight wealth management application for
|
||||
individuals to keep track of stocks, ETFs or cryptocurrencies and make
|
||||
solid, data-driven investment decisions. The source code is fully
|
||||
available as open source software (OSS). The project has been
|
||||
initiated by
|
||||
available as
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio"
|
||||
title="Find Ghostfolio on GitHub"
|
||||
>open source software</a
|
||||
>
|
||||
(OSS) under the
|
||||
<a
|
||||
href="https://www.gnu.org/licenses/agpl-3.0.html"
|
||||
title="GNU Affero General Public License"
|
||||
>AGPL-3.0 license</a
|
||||
>
|
||||
and we share aggregated
|
||||
<a
|
||||
href="https://ghostfol.io/{{ defaultLanguageCode }}/open"
|
||||
title="Open Startup"
|
||||
>key metrics</a
|
||||
>
|
||||
of the platform’s performance. The project has been initiated by
|
||||
<a href="https://dotsilver.ch" title="Website of Thomas Kaul"
|
||||
>Thomas Kaul</a
|
||||
>
|
||||
@ -127,6 +144,7 @@
|
||||
<gf-value
|
||||
size="large"
|
||||
subLabel="(Last 24 hours)"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="statistics?.activeUsers1d ?? '-'"
|
||||
>Active Users</gf-value
|
||||
>
|
||||
@ -135,6 +153,7 @@
|
||||
<gf-value
|
||||
size="large"
|
||||
subLabel="(Last 30 days)"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="statistics?.newUsers30d ?? '-'"
|
||||
>New Users</gf-value
|
||||
>
|
||||
@ -143,6 +162,7 @@
|
||||
<gf-value
|
||||
size="large"
|
||||
subLabel="(Last 30 days)"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="statistics?.activeUsers30d ?? '-'"
|
||||
>Active Users</gf-value
|
||||
>
|
||||
@ -151,6 +171,7 @@
|
||||
<a class="d-block" href="https://ghostfolio.slack.com">
|
||||
<gf-value
|
||||
size="large"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="statistics?.slackCommunityUsers ?? '-'"
|
||||
>Users in Slack community</gf-value
|
||||
>
|
||||
@ -163,6 +184,7 @@
|
||||
>
|
||||
<gf-value
|
||||
size="large"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="statistics?.gitHubContributors ?? '-'"
|
||||
>Contributors on GitHub</gf-value
|
||||
>
|
||||
@ -175,6 +197,7 @@
|
||||
>
|
||||
<gf-value
|
||||
size="large"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="statistics?.gitHubStargazers ?? '-'"
|
||||
>Stars on GitHub</gf-value
|
||||
>
|
||||
@ -198,7 +221,7 @@
|
||||
</div>
|
||||
<div
|
||||
class="col-md-3 col-xs-12 my-2"
|
||||
[ngClass]="{ 'offset-md-4': !hasPermissionForBlog }"
|
||||
[ngClass]="{ 'mx-auto': !hasPermissionForBlog }"
|
||||
>
|
||||
<a
|
||||
class="py-4 w-100"
|
||||
|
@ -3,7 +3,7 @@ import { RouterModule, Routes } from '@angular/router';
|
||||
import { AdminJobsComponent } from '@ghostfolio/client/components/admin-jobs/admin-jobs.component';
|
||||
import { AdminMarketDataComponent } from '@ghostfolio/client/components/admin-market-data/admin-market-data.component';
|
||||
import { AdminOverviewComponent } from '@ghostfolio/client/components/admin-overview/admin-overview.component';
|
||||
import { AdminPlatformComponent } from '@ghostfolio/client/components/admin-platform/platform.component';
|
||||
import { AdminSettingsComponent } from '@ghostfolio/client/components/admin-settings/admin-settings.component';
|
||||
import { AdminUsersComponent } from '@ghostfolio/client/components/admin-users/admin-users.component';
|
||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
|
||||
@ -25,15 +25,15 @@ const routes: Routes = [
|
||||
component: AdminOverviewComponent,
|
||||
title: $localize`Admin Control`
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
component: AdminSettingsComponent,
|
||||
title: $localize`Settings`
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
component: AdminUsersComponent,
|
||||
title: $localize`Users`
|
||||
},
|
||||
{
|
||||
path: 'platforms',
|
||||
component: AdminPlatformComponent,
|
||||
title: $localize`Platforms`
|
||||
}
|
||||
],
|
||||
component: AdminPageComponent,
|
||||
|
@ -30,18 +30,18 @@ export class AdminPageComponent implements OnDestroy, OnInit {
|
||||
label: $localize`Overview`,
|
||||
path: 'overview'
|
||||
},
|
||||
{ iconName: 'people-outline', label: $localize`Users`, path: 'users' },
|
||||
{
|
||||
iconName: 'briefcase-outline',
|
||||
label: $localize`Platforms`,
|
||||
path: 'platforms'
|
||||
iconName: 'settings-outline',
|
||||
label: $localize`Settings`,
|
||||
path: 'settings'
|
||||
},
|
||||
{
|
||||
iconName: 'server-outline',
|
||||
label: $localize`Market Data`,
|
||||
path: 'market-data'
|
||||
},
|
||||
{ iconName: 'flash-outline', label: $localize`Jobs`, path: 'jobs' }
|
||||
{ iconName: 'flash-outline', label: $localize`Jobs`, path: 'jobs' },
|
||||
{ iconName: 'people-outline', label: $localize`Users`, path: 'users' }
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,7 @@ import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { GfAdminJobsModule } from '@ghostfolio/client/components/admin-jobs/admin-jobs.module';
|
||||
import { GfAdminMarketDataModule } from '@ghostfolio/client/components/admin-market-data/admin-market-data.module';
|
||||
import { GfAdminOverviewModule } from '@ghostfolio/client/components/admin-overview/admin-overview.module';
|
||||
import { GfAdminPlatformModule } from '@ghostfolio/client/components/admin-platform/admin-platform.module';
|
||||
import { GfAdminSettingsModule } from '@ghostfolio/client/components/admin-settings/admin-settings.module';
|
||||
import { GfAdminUsersModule } from '@ghostfolio/client/components/admin-users/admin-users.module';
|
||||
import { CacheService } from '@ghostfolio/client/services/cache.service';
|
||||
|
||||
@ -20,7 +20,7 @@ import { AdminPageComponent } from './admin-page.component';
|
||||
GfAdminJobsModule,
|
||||
GfAdminMarketDataModule,
|
||||
GfAdminOverviewModule,
|
||||
GfAdminPlatformModule,
|
||||
GfAdminSettingsModule,
|
||||
GfAdminUsersModule,
|
||||
MatTabsModule
|
||||
],
|
||||
|
@ -14,6 +14,7 @@
|
||||
gf-admin-jobs,
|
||||
gf-admin-market-data,
|
||||
gf-admin-overview,
|
||||
gf-admin-settings,
|
||||
gf-admin-users {
|
||||
flex: 1 1 auto;
|
||||
overflow-y: auto;
|
||||
|
@ -202,7 +202,10 @@
|
||||
<li class="breadcrumb-item">
|
||||
<a i18n [routerLink]="['/blog']">Blog</a>
|
||||
</li>
|
||||
<li aria-current="page" class="breadcrumb-item active">
|
||||
<li
|
||||
aria-current="page"
|
||||
class="active breadcrumb-item text-truncate"
|
||||
>
|
||||
Hallo Ghostfolio
|
||||
</li>
|
||||
</ol>
|
||||
|
@ -182,7 +182,10 @@
|
||||
<li class="breadcrumb-item">
|
||||
<a i18n [routerLink]="['/blog']">Blog</a>
|
||||
</li>
|
||||
<li aria-current="page" class="breadcrumb-item active">
|
||||
<li
|
||||
aria-current="page"
|
||||
class="active breadcrumb-item text-truncate"
|
||||
>
|
||||
Hello Ghostfolio
|
||||
</li>
|
||||
</ol>
|
||||
|
@ -179,7 +179,10 @@
|
||||
<li class="breadcrumb-item">
|
||||
<a i18n [routerLink]="['/blog']">Blog</a>
|
||||
</li>
|
||||
<li aria-current="page" class="breadcrumb-item active">
|
||||
<li
|
||||
aria-current="page"
|
||||
class="active breadcrumb-item text-truncate"
|
||||
>
|
||||
Ghostfolio: First months in Open Source
|
||||
</li>
|
||||
</ol>
|
||||
|
@ -182,7 +182,10 @@
|
||||
<li class="breadcrumb-item">
|
||||
<a i18n [routerLink]="['/blog']">Blog</a>
|
||||
</li>
|
||||
<li aria-current="page" class="breadcrumb-item active">
|
||||
<li
|
||||
aria-current="page"
|
||||
class="active breadcrumb-item text-truncate"
|
||||
>
|
||||
Ghostfolio meets Internet Identity
|
||||
</li>
|
||||
</ol>
|
||||
|
@ -208,7 +208,10 @@
|
||||
<li class="breadcrumb-item">
|
||||
<a i18n [routerLink]="['/blog']">Blog</a>
|
||||
</li>
|
||||
<li aria-current="page" class="breadcrumb-item active">
|
||||
<li
|
||||
aria-current="page"
|
||||
class="active breadcrumb-item text-truncate"
|
||||
>
|
||||
How do I get my finances in order?
|
||||
</li>
|
||||
</ol>
|
||||
|
@ -191,7 +191,10 @@
|
||||
<li class="breadcrumb-item">
|
||||
<a i18n [routerLink]="['/blog']">Blog</a>
|
||||
</li>
|
||||
<li aria-current="page" class="breadcrumb-item active">
|
||||
<li
|
||||
aria-current="page"
|
||||
class="active breadcrumb-item text-truncate"
|
||||
>
|
||||
500 Stars on GitHub
|
||||
</li>
|
||||
</ol>
|
||||
|
@ -177,7 +177,10 @@
|
||||
<li class="breadcrumb-item">
|
||||
<a i18n [routerLink]="['/blog']">Blog</a>
|
||||
</li>
|
||||
<li aria-current="page" class="breadcrumb-item active">
|
||||
<li
|
||||
aria-current="page"
|
||||
class="active breadcrumb-item text-truncate"
|
||||
>
|
||||
Hacktoberfest 2022
|
||||
</li>
|
||||
</ol>
|
||||
|
@ -137,7 +137,10 @@
|
||||
<li class="breadcrumb-item">
|
||||
<a i18n [routerLink]="['/blog']">Blog</a>
|
||||
</li>
|
||||
<li aria-current="page" class="breadcrumb-item active">
|
||||
<li
|
||||
aria-current="page"
|
||||
class="active breadcrumb-item text-truncate"
|
||||
>
|
||||
Black Friday 2022
|
||||
</li>
|
||||
</ol>
|
||||
|
@ -167,7 +167,10 @@
|
||||
<li class="breadcrumb-item">
|
||||
<a i18n [routerLink]="['/blog']">Blog</a>
|
||||
</li>
|
||||
<li aria-current="page" class="breadcrumb-item active">
|
||||
<li
|
||||
aria-current="page"
|
||||
class="active breadcrumb-item text-truncate"
|
||||
>
|
||||
The importance of tracking your personal finances
|
||||
</li>
|
||||
</ol>
|
||||
|
@ -177,7 +177,10 @@
|
||||
<li class="breadcrumb-item">
|
||||
<a i18n [routerLink]="['/blog']">Blog</a>
|
||||
</li>
|
||||
<li aria-current="page" class="breadcrumb-item active">
|
||||
<li
|
||||
aria-current="page"
|
||||
class="active breadcrumb-item text-truncate"
|
||||
>
|
||||
Ghostfolio auf Sackgeld.com vorgestellt
|
||||
</li>
|
||||
</ol>
|
||||
|
@ -199,7 +199,10 @@
|
||||
<li class="breadcrumb-item">
|
||||
<a i18n [routerLink]="['/blog']">Blog</a>
|
||||
</li>
|
||||
<li aria-current="page" class="breadcrumb-item active">
|
||||
<li
|
||||
aria-current="page"
|
||||
class="active breadcrumb-item text-truncate"
|
||||
>
|
||||
Ghostfolio meets Umbrel
|
||||
</li>
|
||||
</ol>
|
||||
|
@ -244,7 +244,10 @@
|
||||
<li class="breadcrumb-item">
|
||||
<a i18n [routerLink]="['/blog']">Blog</a>
|
||||
</li>
|
||||
<li aria-current="page" class="breadcrumb-item active">
|
||||
<li
|
||||
aria-current="page"
|
||||
class="active breadcrumb-item text-truncate"
|
||||
>
|
||||
Ghostfolio reaches 1’000 Stars on GitHub
|
||||
</li>
|
||||
</ol>
|
||||
|
@ -0,0 +1,20 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
|
||||
import { UnlockYourFinancialPotentialWithGhostfolioPageComponent } from './unlock-your-financial-potential-with-ghostfolio-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
component: UnlockYourFinancialPotentialWithGhostfolioPageComponent,
|
||||
path: '',
|
||||
title: 'Unlock your Financial Potential with Ghostfolio'
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class UnlockYourFinancialPotentialWithGhostfolioRoutingModule {}
|
@ -0,0 +1,9 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
selector: 'gf-unlock-your-financial-potential-with-ghostfolio-page',
|
||||
styleUrls: ['./unlock-your-financial-potential-with-ghostfolio-page.scss'],
|
||||
templateUrl: './unlock-your-financial-potential-with-ghostfolio-page.html'
|
||||
})
|
||||
export class UnlockYourFinancialPotentialWithGhostfolioPageComponent {}
|
@ -0,0 +1,244 @@
|
||||
<div class="blog container">
|
||||
<div class="row">
|
||||
<div class="col-md-8 offset-md-2">
|
||||
<article>
|
||||
<div class="mb-4 text-center">
|
||||
<h1 class="mb-1">Unlock your Financial Potential with Ghostfolio</h1>
|
||||
<div class="mb-3 text-muted"><small>2023-05-20</small></div>
|
||||
<img
|
||||
alt="Unlock your financial potential with Ghostfolio Teaser"
|
||||
class="border rounded w-100"
|
||||
src="../assets/images/blog/20230520.jpg"
|
||||
title="Unlock your financial potential with Ghostfolio"
|
||||
/>
|
||||
</div>
|
||||
<section class="mb-4">
|
||||
<p>
|
||||
Managing personal finances effectively is crucial for those striving
|
||||
for a secure future and financial independence. In today’s digital
|
||||
age, having a reliable wealth management software can greatly
|
||||
simplify the process. Ghostfolio is a powerful
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio"
|
||||
title="Find Ghostfolio on GitHub"
|
||||
>open source solution</a
|
||||
>
|
||||
for individuals trading stocks, ETFs, or cryptocurrencies on
|
||||
multiple platforms. This article explores the key reasons why
|
||||
Ghostfolio is the ideal choice for those embracing diversification,
|
||||
pursuing a buy & hold strategy, and seeking portfolio insights while
|
||||
valuing privacy.
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<h2 class="h4">Effortless Management for Multi-Platform Investors</h2>
|
||||
<p>
|
||||
Ghostfolio offers a holistic solution to efficiently monitor and
|
||||
manage investment portfolios across multiple platforms. By
|
||||
consolidating data from various accounts, Ghostfolio eliminates the
|
||||
need to switch between platforms, saving users valuable time and
|
||||
effort.
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<h2 class="h4">Empowering Buy & Hold Strategies</h2>
|
||||
<p>
|
||||
For those committed to a
|
||||
<a [routerLink]="['/resources']">buy & hold strategy</a>, Ghostfolio
|
||||
provides an intuitive interface to monitor long-term investments.
|
||||
Users can track performance over time, gaining insights into
|
||||
portfolio growth and stability. With strong visualizations and
|
||||
reporting <a [routerLink]="['/features']">features</a>, Ghostfolio
|
||||
equips users to make well-informed decisions aligned with their
|
||||
long-term investment goals.
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<h2 class="h4">Deep Portfolio Insights</h2>
|
||||
<p>
|
||||
Understanding portfolio composition is vital for making informed
|
||||
financial decisions. Ghostfolio provides comprehensive insights into
|
||||
asset allocation, sector exposure, geographical diversification, and
|
||||
individual asset performance. These detailed analytics empower users
|
||||
to assess portfolio strengths and weaknesses, making necessary
|
||||
adjustments to optimize their allocation.
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<h2 class="h4">Privacy and Data Ownership</h2>
|
||||
<p>
|
||||
In the age of growing data security concerns, Ghostfolio sets itself
|
||||
apart by giving the highest priority to privacy and data ownership.
|
||||
As an open-source software, Ghostfolio ensures that users retain
|
||||
complete control over their financial data. By eliminating the need
|
||||
to trust third-party platforms with sensitive information,
|
||||
Ghostfolio offers peace of mind to those who value privacy and data
|
||||
security.
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<h2 class="h4">Streamlined Minimalism for Financial Efficiency</h2>
|
||||
<p>
|
||||
Ghostfolio embraces a lightweight approach to personal finance
|
||||
management, focusing on essential features without overwhelming
|
||||
users. Its streamlined user interface and clean design provide a
|
||||
seamless and clutter-free experience. This minimalist approach
|
||||
enhances user satisfaction and boosts efficiency by eliminating
|
||||
distractions and simplifying the financial management process.
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<h2 class="h4">Driving Financial Independence (FIRE)</h2>
|
||||
<p>
|
||||
Achieving
|
||||
<a [routerLink]="['/resources']">financial independence</a>
|
||||
including early retirement (FIRE) requires careful planning,
|
||||
monitoring, and forecasting. Ghostfolio’s robust features equip
|
||||
individuals with tools to analyze, optimize and simulate investment
|
||||
strategies. By providing insights, performance tracking, and
|
||||
portfolio analysis, Ghostfolio serves as a valuable companion in the
|
||||
pursuit of financial freedom.
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<h2 class="h4">Farewell to Spreadsheet Hassles</h2>
|
||||
<p>
|
||||
While spreadsheets have traditionally been used to manage personal
|
||||
finances, they can be time-consuming and prone to errors. Ghostfolio
|
||||
offers a user-friendly alternative by automating data aggregation,
|
||||
analysis, and reporting. Users can bid farewell to manual data entry
|
||||
and complex formulas, relying instead on Ghostfolio’s user-friendly
|
||||
and intuitive interface to efficiently manage their finances.
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<h2 class="h4">Your Path to Financial Success with Ghostfolio</h2>
|
||||
<p>
|
||||
Ghostfolio, the open-source personal finance software, provides a
|
||||
wide range of benefits for individuals involved in trading stocks,
|
||||
ETFs, or cryptocurrencies. Whether you are pursuing a buy & hold
|
||||
strategy, seeking valuable portfolio insights, or diversifying
|
||||
financial resources while prioritizing privacy and data ownership,
|
||||
Ghostfolio proves to be an invaluable tool on your journey towards
|
||||
unlocking your financial potential. Say goodbye to spreadsheets and
|
||||
embrace the power of Ghostfolio for simplified, secure, and
|
||||
successful financial management.
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4 py-3">
|
||||
<h2 class="h4 mb-0 text-center">
|
||||
Would you like to <strong>unlock</strong> your
|
||||
<strong>financial potential</strong>?
|
||||
</h2>
|
||||
<p class="lead mb-2 text-center">
|
||||
Ghostfolio empowers you to manage your personal finances
|
||||
effectively.
|
||||
</p>
|
||||
<div class="text-center">
|
||||
<a color="primary" href="https://ghostfol.io" mat-flat-button>
|
||||
Get Started
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
<ul class="list-inline">
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">App</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Analysis</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Assets</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Budgeting</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Buy & Hold</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Cryptocurrencies</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Diversification</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">ETFs</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Finance</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Fintech</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">FIRE</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Ghostfolio</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Investment</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Management</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Minimalism</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Monitoring</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Open Source</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Personal Finance</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Planning</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Portfolio Tracker</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Privacy</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Retirement</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Software</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Spreadsheet</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Stock</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Strategy</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<span class="badge badge-light">Wealth</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<a i18n [routerLink]="['/blog']">Blog</a>
|
||||
</li>
|
||||
<li
|
||||
aria-current="page"
|
||||
class="active breadcrumb-item text-truncate"
|
||||
>
|
||||
Unlock your Financial Potential with Ghostfolio
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,19 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { UnlockYourFinancialPotentialWithGhostfolioRoutingModule } from './unlock-your-financial-potential-with-ghostfolio-page-routing.module';
|
||||
import { UnlockYourFinancialPotentialWithGhostfolioPageComponent } from './unlock-your-financial-potential-with-ghostfolio-page.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [UnlockYourFinancialPotentialWithGhostfolioPageComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatButtonModule,
|
||||
RouterModule,
|
||||
UnlockYourFinancialPotentialWithGhostfolioRoutingModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class UnlockYourFinancialPotentialWithGhostfolioPageModule {}
|
@ -0,0 +1,3 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
@ -2,6 +2,32 @@
|
||||
<div class="mb-5 row">
|
||||
<div class="col">
|
||||
<h3 class="d-none d-sm-block mb-3 text-center" i18n>Blog</h3>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-content>
|
||||
<div class="container p-0">
|
||||
<div class="flex-nowrap no-gutters row">
|
||||
<a
|
||||
class="d-flex overflow-hidden w-100"
|
||||
href="../en/blog/2023/05/unlock-your-financial-potential-with-ghostfolio"
|
||||
>
|
||||
<div class="flex-grow-1 overflow-hidden">
|
||||
<div class="h6 m-0 text-truncate">
|
||||
Unlock your Financial Potential with Ghostfolio
|
||||
</div>
|
||||
<div class="d-flex text-muted">2023-05-20</div>
|
||||
</div>
|
||||
<div class="align-items-center d-flex">
|
||||
<ion-icon
|
||||
class="chevron text-muted"
|
||||
name="chevron-forward-outline"
|
||||
size="small"
|
||||
></ion-icon>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined" class="mb-3">
|
||||
<mat-card-content>
|
||||
<div class="container p-0">
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { Statistics } from '@ghostfolio/common/interfaces/statistics.interface';
|
||||
import { Statistics } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { format } from 'date-fns';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
|
20
apps/client/src/app/pages/open/open-page-routing.module.ts
Normal file
20
apps/client/src/app/pages/open/open-page-routing.module.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
|
||||
import { OpenPageComponent } from './open-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
component: OpenPageComponent,
|
||||
path: '',
|
||||
title: 'Open Startup'
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class OpenPageRoutingModule {}
|
45
apps/client/src/app/pages/open/open-page.component.ts
Normal file
45
apps/client/src/app/pages/open/open-page.component.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { Statistics, User } from '@ghostfolio/common/interfaces';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
selector: 'gf-open-page',
|
||||
styleUrls: ['./open-page.scss'],
|
||||
templateUrl: './open-page.html'
|
||||
})
|
||||
export class OpenPageComponent implements OnDestroy, OnInit {
|
||||
public statistics: Statistics;
|
||||
public user: User;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor(
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
private userService: UserService
|
||||
) {
|
||||
const { statistics } = this.dataService.fetchInfo();
|
||||
|
||||
this.statistics = statistics;
|
||||
}
|
||||
|
||||
public ngOnInit() {
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((state) => {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
}
|
126
apps/client/src/app/pages/open/open-page.html
Normal file
126
apps/client/src/app/pages/open/open-page.html
Normal file
@ -0,0 +1,126 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3 class="d-none d-sm-block mb-3 text-center">Open Startup</h3>
|
||||
<div class="intro-container">
|
||||
<p>
|
||||
At Ghostfolio, transparency is at the core of our values. We publish
|
||||
the source code as
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio"
|
||||
title="Find Ghostfolio on GitHub"
|
||||
>open source software</a
|
||||
>
|
||||
(OSS) under the
|
||||
<a
|
||||
href="https://www.gnu.org/licenses/agpl-3.0.html"
|
||||
title="GNU Affero General Public License"
|
||||
>AGPL-3.0 license</a
|
||||
>
|
||||
and we openly share aggregated key metrics of the platform’s
|
||||
performance.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<mat-card appearance="outlined">
|
||||
<mat-card-content>
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md-4 my-2">
|
||||
<gf-value
|
||||
size="large"
|
||||
subLabel="(Last 24 hours)"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="statistics?.activeUsers1d ?? '-'"
|
||||
>Active Users</gf-value
|
||||
>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-4 my-2">
|
||||
<gf-value
|
||||
size="large"
|
||||
subLabel="(Last 30 days)"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="statistics?.newUsers30d ?? '-'"
|
||||
>New Users</gf-value
|
||||
>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-4 my-2">
|
||||
<gf-value
|
||||
size="large"
|
||||
subLabel="(Last 30 days)"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="statistics?.activeUsers30d ?? '-'"
|
||||
>Active Users</gf-value
|
||||
>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-4 my-2">
|
||||
<a class="d-block" href="https://ghostfolio.slack.com">
|
||||
<gf-value
|
||||
size="large"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="statistics?.slackCommunityUsers ?? '-'"
|
||||
>Users in Slack community</gf-value
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-4 my-2">
|
||||
<a
|
||||
class="d-block"
|
||||
href="https://github.com/ghostfolio/ghostfolio/graphs/contributors"
|
||||
>
|
||||
<gf-value
|
||||
size="large"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="statistics?.gitHubContributors ?? '-'"
|
||||
>Contributors on GitHub</gf-value
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-4 my-2">
|
||||
<a
|
||||
class="d-block"
|
||||
href="https://github.com/ghostfolio/ghostfolio/stargazers"
|
||||
>
|
||||
<gf-value
|
||||
size="large"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="statistics?.gitHubStargazers ?? '-'"
|
||||
>Stars on GitHub</gf-value
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-4 my-2">
|
||||
<a
|
||||
class="d-block"
|
||||
href="https://hub.docker.com/r/ghostfolio/ghostfolio"
|
||||
>
|
||||
<gf-value
|
||||
size="large"
|
||||
[locale]="user?.settings?.locale"
|
||||
[value]="statistics?.dockerHubPulls ?? '-'"
|
||||
>Pulls on Docker Hub</gf-value
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-4 my-2">
|
||||
<a class="d-block" href="https://status.ghostfol.io">
|
||||
<gf-value
|
||||
size="large"
|
||||
subLabel="(Last 90 days)"
|
||||
[isPercent]="true"
|
||||
[locale]="user?.settings?.locale"
|
||||
[precision]="2"
|
||||
[value]="statistics?.uptime ?? '-'"
|
||||
>Uptime</gf-value
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
14
apps/client/src/app/pages/open/open-page.module.ts
Normal file
14
apps/client/src/app/pages/open/open-page.module.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||
|
||||
import { OpenPageRoutingModule } from './open-page-routing.module';
|
||||
import { OpenPageComponent } from './open-page.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [OpenPageComponent],
|
||||
imports: [CommonModule, GfValueModule, MatCardModule, OpenPageRoutingModule],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class OpenPageModule {}
|
19
apps/client/src/app/pages/open/open-page.scss
Normal file
19
apps/client/src/app/pages/open/open-page.scss
Normal file
@ -0,0 +1,19 @@
|
||||
:host {
|
||||
color: rgb(var(--dark-primary-text));
|
||||
display: block;
|
||||
|
||||
.intro-container {
|
||||
a {
|
||||
color: rgba(var(--palette-primary-500), 1);
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
color: rgba(var(--palette-primary-300), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
color: rgb(var(--light-primary-text));
|
||||
}
|
@ -415,13 +415,14 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
||||
: this.activityForm.controls['searchSymbol'].value.symbol,
|
||||
tags: this.activityForm.controls['tags'].value,
|
||||
type: this.activityForm.controls['type'].value,
|
||||
unitPrice: this.activityForm.controls['unitPrice'].value,
|
||||
updateAccountBalance:
|
||||
this.activityForm.controls['updateAccountBalance'].value
|
||||
unitPrice: this.activityForm.controls['unitPrice'].value
|
||||
};
|
||||
|
||||
if (this.data.activity.id) {
|
||||
(activity as UpdateOrderDto).id = this.data.activity.id;
|
||||
} else {
|
||||
(activity as CreateOrderDto).updateAccountBalance =
|
||||
this.activityForm.controls['updateAccountBalance'].value;
|
||||
}
|
||||
|
||||
this.dialogRef.close({ activity });
|
||||
|
@ -18,8 +18,12 @@
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="mb-1 without-hint w-100">
|
||||
<div [ngClass]="{'mb-3': data.activity.id}">
|
||||
<mat-form-field
|
||||
appearance="outline"
|
||||
class="w-100"
|
||||
[ngClass]="{'mb-1 without-hint': !data.activity.id}"
|
||||
>
|
||||
<mat-label i18n>Account</mat-label>
|
||||
<mat-select formControlName="accountId">
|
||||
<mat-option
|
||||
@ -32,7 +36,7 @@
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="mb-3" [ngClass]="{'d-none': data.activity.id}">
|
||||
<mat-checkbox color="primary" formControlName="updateAccountBalance" i18n
|
||||
>Update Cash Balance</mat-checkbox
|
||||
>
|
||||
|
@ -0,0 +1,4 @@
|
||||
export enum ImportStep {
|
||||
UPLOAD_FILE = 0,
|
||||
SELECT_ACTIVITIES = 1
|
||||
}
|
@ -1,3 +1,7 @@
|
||||
import {
|
||||
StepperOrientation,
|
||||
StepperSelectionEvent
|
||||
} from '@angular/cdk/stepper';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
@ -8,6 +12,7 @@ import {
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { MatStepper } from '@angular/material/stepper';
|
||||
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
|
||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
@ -15,8 +20,10 @@ import { ImportActivitiesService } from '@ghostfolio/client/services/import-acti
|
||||
import { Position } from '@ghostfolio/common/interfaces';
|
||||
import { AssetClass } from '@prisma/client';
|
||||
import { isArray, sortBy } from 'lodash';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
import { ImportStep } from './enums/import-step';
|
||||
import { ImportActivitiesDialogParams } from './interfaces/interfaces';
|
||||
|
||||
@Component({
|
||||
@ -29,12 +36,14 @@ export class ImportActivitiesDialog implements OnDestroy {
|
||||
public accounts: CreateAccountDto[] = [];
|
||||
public activities: Activity[] = [];
|
||||
public details: any[] = [];
|
||||
public deviceType: string;
|
||||
public errorMessages: string[] = [];
|
||||
public holdings: Position[] = [];
|
||||
public isFileSelected = false;
|
||||
public importStep: ImportStep = ImportStep.UPLOAD_FILE;
|
||||
public maxSafeInteger = Number.MAX_SAFE_INTEGER;
|
||||
public mode: 'DIVIDEND';
|
||||
public selectedActivities: Activity[] = [];
|
||||
public stepperOrientation: StepperOrientation;
|
||||
public uniqueAssetForm: FormGroup;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
@ -43,6 +52,7 @@ export class ImportActivitiesDialog implements OnDestroy {
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
@Inject(MAT_DIALOG_DATA) public data: ImportActivitiesDialogParams,
|
||||
private dataService: DataService,
|
||||
private deviceService: DeviceDetectorService,
|
||||
private formBuilder: FormBuilder,
|
||||
public dialogRef: MatDialogRef<ImportActivitiesDialog>,
|
||||
private importActivitiesService: ImportActivitiesService,
|
||||
@ -50,6 +60,10 @@ export class ImportActivitiesDialog implements OnDestroy {
|
||||
) {}
|
||||
|
||||
public ngOnInit() {
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
this.stepperOrientation =
|
||||
this.deviceType === 'mobile' ? 'vertical' : 'horizontal';
|
||||
|
||||
this.uniqueAssetForm = this.formBuilder.group({
|
||||
uniqueAsset: [undefined, Validators.required]
|
||||
});
|
||||
@ -116,7 +130,15 @@ export class ImportActivitiesDialog implements OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
public onLoadDividends() {
|
||||
public onImportStepChange(event: StepperSelectionEvent) {
|
||||
if (event.selectedIndex === ImportStep.UPLOAD_FILE) {
|
||||
this.importStep = ImportStep.UPLOAD_FILE;
|
||||
} else if (event.selectedIndex === ImportStep.SELECT_ACTIVITIES) {
|
||||
this.importStep = ImportStep.SELECT_ACTIVITIES;
|
||||
}
|
||||
}
|
||||
|
||||
public onLoadDividends(aStepper: MatStepper) {
|
||||
this.uniqueAssetForm.controls['uniqueAsset'].disable();
|
||||
|
||||
const { dataSource, symbol } =
|
||||
@ -130,19 +152,23 @@ export class ImportActivitiesDialog implements OnDestroy {
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ activities }) => {
|
||||
this.activities = activities;
|
||||
this.isFileSelected = true;
|
||||
|
||||
aStepper.next();
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
public onReset() {
|
||||
public onReset(aStepper: MatStepper) {
|
||||
this.details = [];
|
||||
this.errorMessages = [];
|
||||
this.isFileSelected = false;
|
||||
this.importStep = ImportStep.SELECT_ACTIVITIES;
|
||||
this.uniqueAssetForm.controls['uniqueAsset'].enable();
|
||||
|
||||
aStepper.reset();
|
||||
}
|
||||
|
||||
public onSelectFile() {
|
||||
public onSelectFile(aStepper: MatStepper) {
|
||||
const input = document.createElement('input');
|
||||
input.accept = 'application/JSON, .csv';
|
||||
input.type = 'file';
|
||||
@ -225,8 +251,11 @@ export class ImportActivitiesDialog implements OnDestroy {
|
||||
error: { error: { message: ['Unexpected format'] } }
|
||||
});
|
||||
} finally {
|
||||
this.isFileSelected = true;
|
||||
this.importStep = ImportStep.SELECT_ACTIVITIES;
|
||||
this.snackBar.dismiss();
|
||||
|
||||
aStepper.next();
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
};
|
||||
@ -235,8 +264,10 @@ export class ImportActivitiesDialog implements OnDestroy {
|
||||
input.click();
|
||||
}
|
||||
|
||||
public updateSelection(data: Activity[]) {
|
||||
this.selectedActivities = data;
|
||||
public updateSelection(activities: Activity[]) {
|
||||
this.selectedActivities = activities.filter(({ error }) => {
|
||||
return !error;
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
|
@ -5,123 +5,152 @@
|
||||
(closeButtonClicked)="onCancel()"
|
||||
></gf-dialog-header>
|
||||
|
||||
<div class="flex-grow-1 py-3" mat-dialog-content>
|
||||
<ng-container *ngIf="!isFileSelected">
|
||||
<ng-container *ngIf="mode === 'DIVIDEND'; else selectFile">
|
||||
<form [formGroup]="uniqueAssetForm" (ngSubmit)="onLoadDividends()">
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Holding</mat-label>
|
||||
<mat-select formControlName="uniqueAsset">
|
||||
<mat-option
|
||||
*ngFor="let holding of holdings"
|
||||
[value]="{dataSource: holding.dataSource, symbol: holding.symbol}"
|
||||
>{{ holding.name }}</mat-option
|
||||
>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<div class="d-flex justify-content-center flex-column">
|
||||
<button
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
type="submit"
|
||||
[disabled]="!uniqueAssetForm.valid"
|
||||
>
|
||||
<span i18n>Load Dividends</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</ng-container>
|
||||
<ng-template #selectFile>
|
||||
<div class="d-flex justify-content-center flex-column">
|
||||
<button
|
||||
class="py-4"
|
||||
color="primary"
|
||||
mat-stroked-button
|
||||
(click)="onSelectFile()"
|
||||
>
|
||||
<ion-icon class="mr-2" name="cloud-upload-outline"></ion-icon>
|
||||
<span i18n>Choose File</span>
|
||||
</button>
|
||||
<p class="mb-0 mt-4 text-center">
|
||||
<span class="mr-1" i18n
|
||||
>The following file formats are supported:</span
|
||||
>
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.csv"
|
||||
target="_blank"
|
||||
>CSV</a
|
||||
>
|
||||
<span class="mx-1" i18n>or</span>
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.json"
|
||||
target="_blank"
|
||||
>JSON</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="isFileSelected">
|
||||
<ng-container *ngIf="errorMessages.length === 0; else errorMessage">
|
||||
<gf-activities-table
|
||||
[activities]="activities"
|
||||
[baseCurrency]="data?.user?.settings?.baseCurrency"
|
||||
[deviceType]="data?.deviceType"
|
||||
[hasPermissionToCreateActivity]="false"
|
||||
[hasPermissionToExportActivities]="false"
|
||||
[hasPermissionToFilter]="false"
|
||||
[hasPermissionToOpenDetails]="false"
|
||||
[locale]="data?.user?.settings?.locale"
|
||||
[pageSize]="maxSafeInteger"
|
||||
[showActions]="false"
|
||||
[showCheckbox]="true"
|
||||
[showFooter]="false"
|
||||
[showSymbolColumn]="false"
|
||||
(selectedActivities)="updateSelection($event)"
|
||||
></gf-activities-table>
|
||||
</ng-container>
|
||||
<ng-template #errorMessage>
|
||||
<mat-accordion displayMode="flat">
|
||||
<mat-expansion-panel
|
||||
*ngFor="let message of errorMessages; let i = index"
|
||||
[disabled]="!details[i]"
|
||||
>
|
||||
<mat-expansion-panel-header class="pl-1">
|
||||
<mat-panel-title>
|
||||
<div class="d-flex">
|
||||
<div class="align-items-center d-flex mr-2">
|
||||
<ion-icon name="warning-outline"></ion-icon>
|
||||
</div>
|
||||
<div>{{ message }}</div>
|
||||
</div>
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<pre
|
||||
*ngIf="details[i]"
|
||||
class="m-0"
|
||||
><code>{{ details[i] | json }}</code></pre>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
<div class="mt-2">
|
||||
<button mat-button (click)="onReset()">
|
||||
<ion-icon class="mr-2" name="arrow-back-outline"></ion-icon>
|
||||
<span i18n>Back</span>
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isFileSelected" class="justify-content-end" mat-dialog-actions>
|
||||
<button i18n mat-button (click)="onCancel()">Cancel</button>
|
||||
<button
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
[disabled]="!selectedActivities?.length"
|
||||
(click)="onImportActivities()"
|
||||
<div class="flex-grow-1" mat-dialog-content>
|
||||
<mat-stepper
|
||||
#stepper
|
||||
[animationDuration]="0"
|
||||
[linear]="true"
|
||||
[orientation]="stepperOrientation"
|
||||
[selectedIndex]="importStep"
|
||||
(selectionChange)="onImportStepChange($event)"
|
||||
>
|
||||
<ng-container i18n>Import</ng-container>
|
||||
</button>
|
||||
<mat-step [completed]="importStep === 0" [selected]="importStep === 0">
|
||||
<ng-template i18n matStepLabel>Select File</ng-template>
|
||||
<div class="pt-3">
|
||||
<ng-container *ngIf="mode === 'DIVIDEND'; else selectFile">
|
||||
<form
|
||||
[formGroup]="uniqueAssetForm"
|
||||
(ngSubmit)="onLoadDividends(stepper)"
|
||||
>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Holding</mat-label>
|
||||
<mat-select formControlName="uniqueAsset">
|
||||
<mat-option
|
||||
*ngFor="let holding of holdings"
|
||||
[value]="{dataSource: holding.dataSource, symbol: holding.symbol}"
|
||||
>{{ holding.name }}</mat-option
|
||||
>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<div class="d-flex flex-column justify-content-center">
|
||||
<button
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
type="submit"
|
||||
[disabled]="!uniqueAssetForm.valid"
|
||||
>
|
||||
<span i18n>Load Dividends</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</ng-container>
|
||||
<ng-template #selectFile>
|
||||
<div class="d-flex flex-column justify-content-center">
|
||||
<button
|
||||
class="py-4"
|
||||
color="primary"
|
||||
mat-stroked-button
|
||||
(click)="onSelectFile(stepper)"
|
||||
>
|
||||
<ion-icon class="mr-2" name="cloud-upload-outline"></ion-icon>
|
||||
<span i18n>Choose File</span>
|
||||
</button>
|
||||
<p class="mb-0 mt-4 text-center">
|
||||
<span class="mr-1" i18n
|
||||
>The following file formats are supported:</span
|
||||
>
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.csv"
|
||||
target="_blank"
|
||||
>CSV</a
|
||||
>
|
||||
<span class="mx-1" i18n>or</span>
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.json"
|
||||
target="_blank"
|
||||
>JSON</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</mat-step>
|
||||
|
||||
<mat-step [completed]="importStep === 1" [selected]="importStep === 1">
|
||||
<ng-template i18n matStepLabel>Select Activities</ng-template>
|
||||
<div class="pt-3">
|
||||
<ng-container *ngIf="errorMessages.length === 0; else errorMessage">
|
||||
<gf-activities-table
|
||||
*ngIf="importStep === 1"
|
||||
[activities]="activities"
|
||||
[baseCurrency]="data?.user?.settings?.baseCurrency"
|
||||
[deviceType]="data?.deviceType"
|
||||
[hasPermissionToCreateActivity]="false"
|
||||
[hasPermissionToExportActivities]="false"
|
||||
[hasPermissionToFilter]="false"
|
||||
[hasPermissionToOpenDetails]="false"
|
||||
[locale]="data?.user?.settings?.locale"
|
||||
[pageSize]="maxSafeInteger"
|
||||
[showActions]="false"
|
||||
[showCheckbox]="true"
|
||||
[showFooter]="false"
|
||||
[showSymbolColumn]="false"
|
||||
(selectedActivities)="updateSelection($event)"
|
||||
></gf-activities-table>
|
||||
<div class="d-flex justify-content-end mt-3">
|
||||
<button mat-button (click)="onReset(stepper)">
|
||||
<ng-container i18n>Back</ng-container>
|
||||
</button>
|
||||
<button
|
||||
class="ml-1"
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
[disabled]="!selectedActivities?.length"
|
||||
(click)="onImportActivities()"
|
||||
>
|
||||
<ng-container i18n>Import</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #errorMessage>
|
||||
<mat-accordion displayMode="flat">
|
||||
<mat-expansion-panel
|
||||
*ngFor="let message of errorMessages; let i = index"
|
||||
[disabled]="!details[i]"
|
||||
>
|
||||
<mat-expansion-panel-header class="pl-1">
|
||||
<mat-panel-title>
|
||||
<div class="d-flex">
|
||||
<div class="align-items-center d-flex mr-2">
|
||||
<ion-icon name="warning-outline"></ion-icon>
|
||||
</div>
|
||||
<div>{{ message }}</div>
|
||||
</div>
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<pre
|
||||
*ngIf="details[i]"
|
||||
class="m-0"
|
||||
><code>{{ details[i] | json }}</code></pre>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
<div class="d-flex justify-content-end mt-3">
|
||||
<button mat-button (click)="onReset(stepper)">
|
||||
<ng-container i18n>Back</ng-container>
|
||||
</button>
|
||||
<button
|
||||
class="ml-1"
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
[disabled]="true"
|
||||
>
|
||||
<ng-container i18n>Import</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</mat-step>
|
||||
</mat-stepper>
|
||||
</div>
|
||||
|
||||
<gf-dialog-footer
|
||||
|
@ -6,6 +6,7 @@ import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatExpansionModule } from '@angular/material/expansion';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatStepperModule } from '@angular/material/stepper';
|
||||
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
|
||||
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
|
||||
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
|
||||
@ -25,6 +26,7 @@ import { ImportActivitiesDialog } from './import-activities-dialog.component';
|
||||
MatExpansionModule,
|
||||
MatFormFieldModule,
|
||||
MatSelectModule,
|
||||
MatStepperModule,
|
||||
ReactiveFormsModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
|
@ -5,6 +5,16 @@
|
||||
color: rgba(var(--palette-primary-500), 1);
|
||||
}
|
||||
|
||||
mat-stepper {
|
||||
::ng-deep {
|
||||
.mat-step-header {
|
||||
&[aria-selected='false'] {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mat-expansion-panel {
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
|
@ -405,6 +405,10 @@ export class DataService {
|
||||
return this.http.post<OrderModel>(`/api/v1/account`, aAccount);
|
||||
}
|
||||
|
||||
public postBenchmark(benchmark: UniqueAsset) {
|
||||
return this.http.post(`/api/v1/benchmark`, benchmark);
|
||||
}
|
||||
|
||||
public postOrder(aOrder: CreateOrderDto) {
|
||||
return this.http.post<OrderModel>(`/api/v1/order`, aOrder);
|
||||
}
|
||||
|
BIN
apps/client/src/assets/images/blog/20230520.jpg
Normal file
BIN
apps/client/src/assets/images/blog/20230520.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 103 KiB |
@ -1,6 +1,19 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
Disallow: /de/about/*
|
||||
Disallow: /de/faq
|
||||
Disallow: /de/markets
|
||||
Disallow: /de/portfolio/*
|
||||
Disallow: /de/pricing
|
||||
Disallow: /de/register
|
||||
Disallow: /de/resources
|
||||
Disallow: /de/ueber-uns/datenschutzbestimmungen
|
||||
Disallow: /en/about/privacy-policy
|
||||
Disallow: /en/p/*
|
||||
Disallow: /en/portfolio/*
|
||||
Disallow: /portfolio/*
|
||||
Disallow: /pricing/*
|
||||
Disallow: /register/*
|
||||
Disallow: /resources/*
|
||||
|
||||
Sitemap: https://ghostfol.io/sitemap.xml
|
||||
|
@ -6,102 +6,262 @@
|
||||
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
||||
<url>
|
||||
<loc>https://ghostfol.io</loc>
|
||||
<lastmod>2023-03-25T00:00:00+00:00</lastmod>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/blog</loc>
|
||||
<lastmod>2023-03-25T00:00:00+00:00</lastmod>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/blog/2021/07/hallo-ghostfolio</loc>
|
||||
<lastmod>2023-03-25T00:00:00+00:00</lastmod>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/blog/2023/01/ghostfolio-auf-sackgeld-vorgestellt</loc>
|
||||
<lastmod>2023-03-25T00:00:00+00:00</lastmod>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/pricing</loc>
|
||||
<lastmod>2023-03-25T00:00:00+00:00</lastmod>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/features</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/haeufig-gestellte-fragen</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/maerkte</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/open</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/preise</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/registrierung</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ressourcen</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ueber-uns</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/de/ueber-uns/changelog</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/about</loc>
|
||||
<lastmod>2023-03-25T00:00:00+00:00</lastmod>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/about/changelog</loc>
|
||||
<lastmod>2023-03-25T00:00:00+00:00</lastmod>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/blog</loc>
|
||||
<lastmod>2023-03-25T00:00:00+00:00</lastmod>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/blog/2021/07/hello-ghostfolio</loc>
|
||||
<lastmod>2023-03-25T00:00:00+00:00</lastmod>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/blog/2022/01/ghostfolio-first-months-in-open-source</loc>
|
||||
<lastmod>2023-03-25T00:00:00+00:00</lastmod>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/blog/2022/07/ghostfolio-meets-internet-identity</loc>
|
||||
<lastmod>2023-03-25T00:00:00+00:00</lastmod>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/blog/2022/07/how-do-i-get-my-finances-in-order</loc>
|
||||
<lastmod>2023-03-25T00:00:00+00:00</lastmod>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/blog/2022/08/500-stars-on-github</loc>
|
||||
<lastmod>2023-03-25T00:00:00+00:00</lastmod>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/blog/2022/10/hacktoberfest-2022</loc>
|
||||
<lastmod>2023-03-25T00:00:00+00:00</lastmod>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/blog/2022/11/black-friday-2022</loc>
|
||||
<lastmod>2023-03-25T00:00:00+00:00</lastmod>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/blog/2022/12/the-importance-of-tracking-your-personal-finances</loc>
|
||||
<lastmod>2023-03-25T00:00:00+00:00</lastmod>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/blog/2023/02/ghostfolio-meets-umbrel</loc>
|
||||
<lastmod>2023-03-25T00:00:00+00:00</lastmod>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/blog/2023/03/ghostfolio-reaches-1000-stars-on-github</loc>
|
||||
<lastmod>2023-03-25T00:00:00+00:00</lastmod>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/blog/2023/05/unlock-your-financial-potential-with-ghostfolio</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/demo</loc>
|
||||
<lastmod>2023-03-25T00:00:00+00:00</lastmod>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/faq</loc>
|
||||
<lastmod>2023-03-25T00:00:00+00:00</lastmod>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/features</loc>
|
||||
<lastmod>2023-03-25T00:00:00+00:00</lastmod>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/markets</loc>
|
||||
<lastmod>2023-03-25T00:00:00+00:00</lastmod>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/open</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/pricing</loc>
|
||||
<lastmod>2023-03-25T00:00:00+00:00</lastmod>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/register</loc>
|
||||
<lastmod>2023-03-25T00:00:00+00:00</lastmod>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/en/resources</loc>
|
||||
<lastmod>2023-03-25T00:00:00+00:00</lastmod>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/fr/a-propos</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/fr/a-propos/changelog</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/fr/a-propos/politique-de-confidentialite</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/fr/enregistrement</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/fr/fonctionnalites</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/fr/foire-aux-questions</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/fr/marches</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/fr/open</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/fr/prix</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/fr/ressources</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/domande-piu-frequenti</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/funzionalita</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/informazioni-su</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/informazioni-su/changelog</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/informazioni-su/informativa-sulla-privacy</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/iscrizione</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/mercati</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/open</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/prezzi</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/it/risorse</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/bronnen</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/kenmerken</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/markten</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/open</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/over</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/over/changelog</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/over/privacybeleid</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/prijzen</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/registratie</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://ghostfol.io/nl/vaak-gestelde-vragen</loc>
|
||||
<lastmod>2023-05-27T00:00:00+00:00</lastmod>
|
||||
</url>
|
||||
</urlset>
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -327,10 +327,13 @@ ngx-skeleton-loader {
|
||||
|
||||
.breadcrumb {
|
||||
background-color: unset;
|
||||
flex-wrap: nowrap;
|
||||
padding: unset;
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
flex-wrap: nowrap;
|
||||
|
||||
&.active {
|
||||
color: unset;
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user