Compare commits
71 Commits
Author | SHA1 | Date | |
---|---|---|---|
382fe24f29 | |||
908876ca6e | |||
99cf9f8802 | |||
7444ff97fc | |||
834a48466e | |||
a9526430c2 | |||
fce3b2084e | |||
f5a50a95de | |||
06dfb91f82 | |||
be36050d76 | |||
7931e6950d | |||
04eb452e04 | |||
6f7e370fca | |||
b4a126280f | |||
2d009aacc4 | |||
9116443305 | |||
0adaf12a01 | |||
b6562b6e2c | |||
b0a4b09ef5 | |||
ad8b9ad333 | |||
809956f210 | |||
6077bfa754 | |||
09498bd804 | |||
fd84f4ec14 | |||
c711a11d6e | |||
8232b05f62 | |||
0ea66aebcb | |||
64087de3fc | |||
7082ff12f8 | |||
1c7d92e15e | |||
a53461d257 | |||
d630fb900d | |||
51e8555fa5 | |||
9db675b955 | |||
45bd8ed029 | |||
707fd31550 | |||
6e5f0086a1 | |||
97bcd8ff49 | |||
1809fc8a80 | |||
beb24f9bd4 | |||
ae57a188f5 | |||
23db85e940 | |||
bd8bb1a36a | |||
c48670ccdc | |||
fc019002e2 | |||
4282cb66b8 | |||
1d0ba5fe4b | |||
24cfb26c5b | |||
26a70aa208 | |||
ab7e050066 | |||
26b1fd6572 | |||
d7e682b65a | |||
f589ccb775 | |||
206b6567fd | |||
6857e0314f | |||
c8682a7393 | |||
144b6b2211 | |||
16a5ace4be | |||
b24ddc30c9 | |||
19333ab084 | |||
7529a7a26c | |||
21ebaae6ef | |||
3bc8b3c836 | |||
bb9415cc15 | |||
b3baeb8a5d | |||
1f393e78f6 | |||
215f5eafa6 | |||
1916e5343d | |||
fa9863fc54 | |||
7bf48ef351 | |||
faef3606fd |
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
|
||||
|
2
.github/workflows/build-code.yml
vendored
2
.github/workflows/build-code.yml
vendored
@ -10,7 +10,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
node_version:
|
||||
- 16
|
||||
- 18
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
132
CHANGELOG.md
132
CHANGELOG.md
@ -5,6 +5,135 @@ 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.281.0 - 2023-06-17
|
||||
|
||||
### Added
|
||||
|
||||
- Extended the feature overview page by liabilities
|
||||
- Set up the language localization for Português (`pt`)
|
||||
|
||||
### Changed
|
||||
|
||||
- Extracted the symbol search to a dedicated component
|
||||
- Improved the column headers in the holdings table for mobile
|
||||
- Upgraded `prisma` from version `4.14.1` to `4.15.0`
|
||||
|
||||
## 1.280.1 - 2023-06-10
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for liabilities
|
||||
|
||||
## 1.279.0 - 2023-06-10
|
||||
|
||||
### Added
|
||||
|
||||
- Supported a note for accounts
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the language localization for French (`fr`)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue with the value nullification related to the investment streaks
|
||||
- Fixed an issue in the public page related to the impersonation service
|
||||
|
||||
## 1.278.0 - 2023-06-09
|
||||
|
||||
### Changed
|
||||
|
||||
- Extended the clone functionality of a transaction by the quantity
|
||||
- Changed the direction of the ellipsis icon in various tables
|
||||
- Extracted the license to a dedicated tab on the about page
|
||||
- Displayed the link to the markets overview in the footer based on a permission
|
||||
- Improved the spacing in the benchmark comparator
|
||||
- Refreshed the cryptocurrencies list
|
||||
- Upgraded `Node.js` from version `16` to `18` (`Dockerfile`)
|
||||
|
||||
## 1.277.0 - 2023-06-07
|
||||
|
||||
### Added
|
||||
|
||||
- Added the investment streaks to the analysis page
|
||||
- Added support for a unit in the value component
|
||||
- Added a semantic list structure to the header navigation
|
||||
- Added a default value for the `includeHistoricalData` attribute in the symbol data endpoint
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue with the date format parsing in the activities import
|
||||
|
||||
## 1.276.0 - 2023-06-03
|
||||
|
||||
### Added
|
||||
|
||||
- Added tabs to the about page
|
||||
- Added the `changefreq` attribute to the sitemap
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the routes of the tabs
|
||||
- Enforced a stricter date format in the activities import: `dd-MM-yyyy` instead of `dd-MM-yy`
|
||||
- Updated the URL of the Ghostfolio Slack channel
|
||||
- Removed the _Ghostfolio in Numbers_ section from the about page
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue with the price when creating a `Subscription`
|
||||
|
||||
## 1.275.0 - 2023-05-30
|
||||
|
||||
### Changed
|
||||
|
||||
- Extended the footer navigation by the localized Ghostfolio versions
|
||||
- Improved the language localization for German (`de`)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the exchange rate service for a specific date (indirect calculation via base currency) used in activities with a manual currency
|
||||
|
||||
## 1.274.0 - 2023-05-29
|
||||
|
||||
### Added
|
||||
|
||||
- Extended the footer by a navigation
|
||||
- Extended the testimonial section on the landing page
|
||||
- Added localized meta descriptions
|
||||
- Added support for localized routes in Spanish (`es`)
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the activities import dialog
|
||||
- Improved the language localization for German (`de`)
|
||||
|
||||
## 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
|
||||
@ -263,7 +392,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`
|
||||
@ -683,7 +812,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Added support for the dividend timeline grouped by year
|
||||
- Added support for the investment timeline grouped by year
|
||||
- Set up the language localization for Français (`fr`)
|
||||
- Set up the language localization for Português (`pt`)
|
||||
|
||||
### Changed
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM --platform=$BUILDPLATFORM node:16-slim as builder
|
||||
FROM --platform=$BUILDPLATFORM node:18-slim as builder
|
||||
|
||||
# Build application and add additional files
|
||||
WORKDIR /ghostfolio
|
||||
@ -50,7 +50,7 @@ COPY package.json /ghostfolio/dist/apps/api
|
||||
RUN yarn database:generate-typings
|
||||
|
||||
# Image to run, copy everything needed from builder
|
||||
FROM node:16-slim
|
||||
FROM node:18-slim
|
||||
RUN apt update && apt install -y \
|
||||
openssl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
@ -145,7 +145,7 @@ Please follow the instructions of the Ghostfolio [Unraid Community App](https://
|
||||
### Prerequisites
|
||||
|
||||
- [Docker](https://www.docker.com/products/docker-desktop)
|
||||
- [Node.js](https://nodejs.org/en/download) (version 16)
|
||||
- [Node.js](https://nodejs.org/en/download) (version 18+)
|
||||
- [Yarn](https://yarnpkg.com/en/docs/install)
|
||||
- Create a local copy of this Git repository (clone)
|
||||
- Copy the file `.env.example` to `.env` and populate it with your data (`cp .env.example .env`)
|
||||
@ -269,7 +269,7 @@ Deprecated: `GET http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TO
|
||||
|
||||
Ghostfolio is **100% free** and **open source**. We encourage and support an active and healthy community that accepts contributions from the public - including you.
|
||||
|
||||
Not sure what to work on? We have got some ideas. Please join the Ghostfolio [Slack channel](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg) or tweet to [@ghostfolio\_](https://twitter.com/ghostfolio_). We would love to hear from you.
|
||||
Not sure what to work on? We have got some ideas. Please join the Ghostfolio [Slack](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg) channel or tweet to [@ghostfolio\_](https://twitter.com/ghostfolio_). We would love to hear from you.
|
||||
|
||||
If you like to support this project, get [**Ghostfolio Premium**](https://ghostfol.io/en/pricing) or [**Buy me a coffee**](https://www.buymeacoffee.com/ghostfolio).
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { AccountType } from '@prisma/client';
|
||||
import { Transform, TransformFnParams } from 'class-transformer';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsNumber,
|
||||
@ -6,6 +7,7 @@ import {
|
||||
IsString,
|
||||
ValidateIf
|
||||
} from 'class-validator';
|
||||
import { isString } from 'lodash';
|
||||
|
||||
export class CreateAccountDto {
|
||||
@IsString()
|
||||
@ -14,6 +16,13 @@ export class CreateAccountDto {
|
||||
@IsNumber()
|
||||
balance: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Transform(({ value }: TransformFnParams) =>
|
||||
isString(value) ? value.trim() : value
|
||||
)
|
||||
comment?: string;
|
||||
|
||||
@IsString()
|
||||
currency: string;
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { AccountType } from '@prisma/client';
|
||||
import { Transform, TransformFnParams } from 'class-transformer';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsNumber,
|
||||
@ -6,6 +7,7 @@ import {
|
||||
IsString,
|
||||
ValidateIf
|
||||
} from 'class-validator';
|
||||
import { isString } from 'lodash';
|
||||
|
||||
export class UpdateAccountDto {
|
||||
@IsString()
|
||||
@ -14,6 +16,13 @@ export class UpdateAccountDto {
|
||||
@IsNumber()
|
||||
balance: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Transform(({ value }: TransformFnParams) =>
|
||||
isString(value) ? value.trim() : value
|
||||
)
|
||||
comment?: string;
|
||||
|
||||
@IsString()
|
||||
currency: string;
|
||||
|
||||
|
@ -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';
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ export class ExportService {
|
||||
select: {
|
||||
accountType: true,
|
||||
balance: true,
|
||||
comment: true,
|
||||
currency: true,
|
||||
id: true,
|
||||
isExcluded: true,
|
||||
|
@ -19,6 +19,9 @@ export class FrontendMiddleware implements NestMiddleware {
|
||||
public indexHtmlNl = '';
|
||||
public indexHtmlPt = '';
|
||||
|
||||
private static readonly DEFAULT_DESCRIPTION =
|
||||
'Ghostfolio is a personal finance dashboard to keep track of your assets like stocks, ETFs or cryptocurrencies across multiple platforms.';
|
||||
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService
|
||||
) {
|
||||
@ -116,6 +119,8 @@ export class FrontendMiddleware implements NestMiddleware {
|
||||
currentDate,
|
||||
featureGraphicPath,
|
||||
title,
|
||||
description:
|
||||
'Mit dem Finanz-Dashboard Ghostfolio können Sie Ihr Vermögen in Form von Aktien, ETFs oder Kryptowährungen verteilt über mehrere Finanzinstitute überwachen.',
|
||||
languageCode: 'de',
|
||||
path: request.path,
|
||||
rootUrl: this.configurationService.get('ROOT_URL')
|
||||
@ -127,6 +132,8 @@ export class FrontendMiddleware implements NestMiddleware {
|
||||
currentDate,
|
||||
featureGraphicPath,
|
||||
title,
|
||||
description:
|
||||
'Ghostfolio es un dashboard de finanzas personales para hacer un seguimiento de tus activos como acciones, ETFs o criptodivisas a través de múltiples plataformas.',
|
||||
languageCode: 'es',
|
||||
path: request.path,
|
||||
rootUrl: this.configurationService.get('ROOT_URL')
|
||||
@ -135,7 +142,11 @@ export class FrontendMiddleware implements NestMiddleware {
|
||||
} else if (request.path === '/fr' || request.path.startsWith('/fr/')) {
|
||||
response.send(
|
||||
this.interpolate(this.indexHtmlFr, {
|
||||
currentDate,
|
||||
featureGraphicPath,
|
||||
title,
|
||||
description:
|
||||
'Ghostfolio est un dashboard de finances personnelles qui permet de suivre vos actifs comme les actions, les ETF ou les crypto-monnaies sur plusieurs plateformes.',
|
||||
languageCode: 'fr',
|
||||
path: request.path,
|
||||
rootUrl: this.configurationService.get('ROOT_URL')
|
||||
@ -147,6 +158,8 @@ export class FrontendMiddleware implements NestMiddleware {
|
||||
currentDate,
|
||||
featureGraphicPath,
|
||||
title,
|
||||
description:
|
||||
'Ghostfolio è un dashboard di finanza personale per tenere traccia delle vostre attività come azioni, ETF o criptovalute su più piattaforme.',
|
||||
languageCode: 'it',
|
||||
path: request.path,
|
||||
rootUrl: this.configurationService.get('ROOT_URL')
|
||||
@ -158,6 +171,8 @@ export class FrontendMiddleware implements NestMiddleware {
|
||||
currentDate,
|
||||
featureGraphicPath,
|
||||
title,
|
||||
description:
|
||||
'Ghostfolio is een persoonlijk financieel dashboard om uw activa zoals aandelen, ETF’s of cryptocurrencies over meerdere platforms bij te houden.',
|
||||
languageCode: 'nl',
|
||||
path: request.path,
|
||||
rootUrl: this.configurationService.get('ROOT_URL')
|
||||
@ -166,7 +181,11 @@ export class FrontendMiddleware implements NestMiddleware {
|
||||
} else if (request.path === '/pt' || request.path.startsWith('/pt/')) {
|
||||
response.send(
|
||||
this.interpolate(this.indexHtmlPt, {
|
||||
currentDate,
|
||||
featureGraphicPath,
|
||||
title,
|
||||
description:
|
||||
'Ghostfolio é um dashboard de finanças pessoais para acompanhar os seus activos como acções, ETFs ou criptomoedas em múltiplas plataformas.',
|
||||
languageCode: 'pt',
|
||||
path: request.path,
|
||||
rootUrl: this.configurationService.get('ROOT_URL')
|
||||
@ -178,6 +197,7 @@ export class FrontendMiddleware implements NestMiddleware {
|
||||
currentDate,
|
||||
featureGraphicPath,
|
||||
title,
|
||||
description: FrontendMiddleware.DEFAULT_DESCRIPTION,
|
||||
languageCode: DEFAULT_LANGUAGE_CODE,
|
||||
path: request.path,
|
||||
rootUrl: this.configurationService.get('ROOT_URL')
|
||||
|
@ -202,7 +202,7 @@ export class ImportService {
|
||||
|
||||
for (const activity of activitiesDto) {
|
||||
if (!activity.dataSource) {
|
||||
if (activity.type === 'ITEM') {
|
||||
if (activity.type === 'ITEM' || activity.type === 'LIABILITY') {
|
||||
activity.dataSource = DataSource.MANUAL;
|
||||
} else {
|
||||
activity.dataSource =
|
||||
|
@ -96,7 +96,7 @@ export class OrderService {
|
||||
const updateAccountBalance = data.updateAccountBalance ?? false;
|
||||
const userId = data.userId;
|
||||
|
||||
if (data.type === 'ITEM') {
|
||||
if (data.type === 'ITEM' || data.type === 'LIABILITY') {
|
||||
const assetClass = data.assetClass;
|
||||
const assetSubClass = data.assetSubClass;
|
||||
currency = data.SymbolProfile.connectOrCreate.create.currency;
|
||||
@ -129,7 +129,10 @@ export class OrderService {
|
||||
}
|
||||
});
|
||||
|
||||
const isDraft = isAfter(data.date as Date, endOfToday());
|
||||
const isDraft =
|
||||
data.type === 'LIABILITY'
|
||||
? false
|
||||
: isAfter(data.date as Date, endOfToday());
|
||||
|
||||
if (!isDraft) {
|
||||
// Gather symbol data of order in the background, if not draft
|
||||
@ -201,7 +204,7 @@ export class OrderService {
|
||||
where
|
||||
});
|
||||
|
||||
if (order.type === 'ITEM') {
|
||||
if (order.type === 'ITEM' || order.type === 'LIABILITY') {
|
||||
await this.symbolProfileService.deleteById(order.symbolProfileId);
|
||||
}
|
||||
|
||||
@ -320,7 +323,11 @@ export class OrderService {
|
||||
})
|
||||
)
|
||||
.filter((order) => {
|
||||
return withExcludedAccounts || order.Account?.isExcluded === false;
|
||||
return (
|
||||
withExcludedAccounts ||
|
||||
!order.Account ||
|
||||
order.Account?.isExcluded === false
|
||||
);
|
||||
})
|
||||
.map((order) => {
|
||||
const value = new Big(order.quantity).mul(order.unitPrice).toNumber();
|
||||
@ -368,7 +375,7 @@ export class OrderService {
|
||||
|
||||
let isDraft = false;
|
||||
|
||||
if (data.type === 'ITEM') {
|
||||
if (data.type === 'ITEM' || data.type === 'LIABILITY') {
|
||||
delete data.SymbolProfile.connect;
|
||||
} else {
|
||||
delete data.SymbolProfile.update;
|
||||
|
@ -105,6 +105,40 @@ describe('PortfolioCalculator', () => {
|
||||
|
||||
expect(investmentsByMonth).toEqual([
|
||||
{ date: '2015-01-01', investment: new Big('640.86') },
|
||||
{ date: '2015-02-01', investment: new Big('0') },
|
||||
{ date: '2015-03-01', investment: new Big('0') },
|
||||
{ date: '2015-04-01', investment: new Big('0') },
|
||||
{ date: '2015-05-01', investment: new Big('0') },
|
||||
{ date: '2015-06-01', investment: new Big('0') },
|
||||
{ date: '2015-07-01', investment: new Big('0') },
|
||||
{ date: '2015-08-01', investment: new Big('0') },
|
||||
{ date: '2015-09-01', investment: new Big('0') },
|
||||
{ date: '2015-10-01', investment: new Big('0') },
|
||||
{ date: '2015-11-01', investment: new Big('0') },
|
||||
{ date: '2015-12-01', investment: new Big('0') },
|
||||
{ date: '2016-01-01', investment: new Big('0') },
|
||||
{ date: '2016-02-01', investment: new Big('0') },
|
||||
{ date: '2016-03-01', investment: new Big('0') },
|
||||
{ date: '2016-04-01', investment: new Big('0') },
|
||||
{ date: '2016-05-01', investment: new Big('0') },
|
||||
{ date: '2016-06-01', investment: new Big('0') },
|
||||
{ date: '2016-07-01', investment: new Big('0') },
|
||||
{ date: '2016-08-01', investment: new Big('0') },
|
||||
{ date: '2016-09-01', investment: new Big('0') },
|
||||
{ date: '2016-10-01', investment: new Big('0') },
|
||||
{ date: '2016-11-01', investment: new Big('0') },
|
||||
{ date: '2016-12-01', investment: new Big('0') },
|
||||
{ date: '2017-01-01', investment: new Big('0') },
|
||||
{ date: '2017-02-01', investment: new Big('0') },
|
||||
{ date: '2017-03-01', investment: new Big('0') },
|
||||
{ date: '2017-04-01', investment: new Big('0') },
|
||||
{ date: '2017-05-01', investment: new Big('0') },
|
||||
{ date: '2017-06-01', investment: new Big('0') },
|
||||
{ date: '2017-07-01', investment: new Big('0') },
|
||||
{ date: '2017-08-01', investment: new Big('0') },
|
||||
{ date: '2017-09-01', investment: new Big('0') },
|
||||
{ date: '2017-10-01', investment: new Big('0') },
|
||||
{ date: '2017-11-01', investment: new Big('0') },
|
||||
{ date: '2017-12-01', investment: new Big('-14156.4') }
|
||||
]);
|
||||
});
|
||||
|
@ -544,7 +544,7 @@ export class PortfolioCalculator {
|
||||
return [];
|
||||
}
|
||||
|
||||
const investments = [];
|
||||
const investments: { date: string; investment: Big }[] = [];
|
||||
let currentDate: Date;
|
||||
let investmentByGroup = new Big(0);
|
||||
|
||||
@ -554,13 +554,11 @@ export class PortfolioCalculator {
|
||||
(groupBy === 'year' || isSameMonth(parseDate(order.date), currentDate))
|
||||
) {
|
||||
// Same group: Add up investments
|
||||
|
||||
investmentByGroup = investmentByGroup.plus(
|
||||
order.quantity.mul(order.unitPrice).mul(this.getFactor(order.type))
|
||||
);
|
||||
} else {
|
||||
// New group: Store previous group and reset
|
||||
|
||||
if (currentDate) {
|
||||
investments.push({
|
||||
date: format(
|
||||
@ -595,7 +593,39 @@ export class PortfolioCalculator {
|
||||
}
|
||||
}
|
||||
|
||||
return investments;
|
||||
// Fill in the missing dates with investment = 0
|
||||
const startDate = parseDate(first(this.orders).date);
|
||||
const endDate = parseDate(last(this.orders).date);
|
||||
|
||||
const allDates: string[] = [];
|
||||
currentDate = startDate;
|
||||
|
||||
while (currentDate <= endDate) {
|
||||
allDates.push(
|
||||
format(
|
||||
set(currentDate, {
|
||||
date: 1,
|
||||
month: groupBy === 'year' ? 0 : currentDate.getMonth()
|
||||
}),
|
||||
DATE_FORMAT
|
||||
)
|
||||
);
|
||||
currentDate.setMonth(currentDate.getMonth() + 1);
|
||||
}
|
||||
|
||||
for (const date of allDates) {
|
||||
const existingInvestment = investments.find((investment) => {
|
||||
return investment.date === date;
|
||||
});
|
||||
|
||||
if (!existingInvestment) {
|
||||
investments.push({ date, investment: new Big(0) });
|
||||
}
|
||||
}
|
||||
|
||||
return sortBy(investments, (investment) => {
|
||||
return investment.date;
|
||||
});
|
||||
}
|
||||
|
||||
public async calculateTimeline(
|
||||
|
@ -162,6 +162,7 @@ export class PortfolioController {
|
||||
'excludedAccountsAndActivities',
|
||||
'fees',
|
||||
'items',
|
||||
'liabilities',
|
||||
'netWorth',
|
||||
'totalBuy',
|
||||
'totalSell'
|
||||
@ -258,11 +259,12 @@ export class PortfolioController {
|
||||
filterByTags
|
||||
});
|
||||
|
||||
let investments = await this.portfolioService.getInvestments({
|
||||
let { investments, streaks } = await this.portfolioService.getInvestments({
|
||||
dateRange,
|
||||
filters,
|
||||
groupBy,
|
||||
impersonationId
|
||||
impersonationId,
|
||||
savingsRate: this.request.user?.Settings?.settings.savingsRate
|
||||
});
|
||||
|
||||
if (
|
||||
@ -278,6 +280,11 @@ export class PortfolioController {
|
||||
date: item.date,
|
||||
investment: item.investment / maxInvestment
|
||||
}));
|
||||
|
||||
streaks = nullifyValuesInObject(streaks, [
|
||||
'currentStreak',
|
||||
'longestStreak'
|
||||
]);
|
||||
}
|
||||
|
||||
if (
|
||||
@ -287,9 +294,14 @@ export class PortfolioController {
|
||||
investments = investments.map((item) => {
|
||||
return nullifyValuesInObject(item, ['investment']);
|
||||
});
|
||||
|
||||
streaks = nullifyValuesInObject(streaks, [
|
||||
'currentStreak',
|
||||
'longestStreak'
|
||||
]);
|
||||
}
|
||||
|
||||
return { investments };
|
||||
return { investments, streaks };
|
||||
}
|
||||
|
||||
@Get('performance')
|
||||
|
@ -28,6 +28,7 @@ import {
|
||||
Filter,
|
||||
HistoricalDataItem,
|
||||
PortfolioDetails,
|
||||
PortfolioInvestments,
|
||||
PortfolioPerformanceResponse,
|
||||
PortfolioPosition,
|
||||
PortfolioReport,
|
||||
@ -252,13 +253,15 @@ export class PortfolioService {
|
||||
dateRange,
|
||||
filters,
|
||||
groupBy,
|
||||
impersonationId
|
||||
impersonationId,
|
||||
savingsRate
|
||||
}: {
|
||||
dateRange: DateRange;
|
||||
filters?: Filter[];
|
||||
groupBy?: GroupBy;
|
||||
impersonationId: string;
|
||||
}): Promise<InvestmentItem[]> {
|
||||
savingsRate: number;
|
||||
}): Promise<PortfolioInvestments> {
|
||||
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
||||
|
||||
const { portfolioOrders, transactionPoints } =
|
||||
@ -276,7 +279,10 @@ export class PortfolioService {
|
||||
|
||||
portfolioCalculator.setTransactionPoints(transactionPoints);
|
||||
if (transactionPoints.length === 0) {
|
||||
return [];
|
||||
return {
|
||||
investments: [],
|
||||
streaks: { currentStreak: 0, longestStreak: 0 }
|
||||
};
|
||||
}
|
||||
|
||||
let investments: InvestmentItem[];
|
||||
@ -346,9 +352,23 @@ export class PortfolioService {
|
||||
parseDate(investments[0]?.date)
|
||||
);
|
||||
|
||||
return investments.filter(({ date }) => {
|
||||
investments = investments.filter(({ date }) => {
|
||||
return !isBefore(parseDate(date), startDate);
|
||||
});
|
||||
|
||||
let streaks: PortfolioInvestments['streaks'];
|
||||
|
||||
if (savingsRate) {
|
||||
streaks = this.getStreaks({
|
||||
investments,
|
||||
savingsRate: groupBy === 'year' ? 12 * savingsRate : savingsRate
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
investments,
|
||||
streaks
|
||||
};
|
||||
}
|
||||
|
||||
public async getChart({
|
||||
@ -1282,12 +1302,11 @@ export class PortfolioService {
|
||||
}: {
|
||||
activities: OrderWithAccount[];
|
||||
date?: Date;
|
||||
|
||||
userCurrency: string;
|
||||
}) {
|
||||
return activities
|
||||
.filter((activity) => {
|
||||
// Filter out all activities before given date and type dividend
|
||||
// Filter out all activities before given date (drafts) and type dividend
|
||||
return (
|
||||
isBefore(date, new Date(activity.date)) &&
|
||||
activity.type === TypeOfOrder.DIVIDEND
|
||||
@ -1411,7 +1430,7 @@ export class PortfolioService {
|
||||
}) {
|
||||
return activities
|
||||
.filter((activity) => {
|
||||
// Filter out all activities before given date
|
||||
// Filter out all activities before given date (drafts)
|
||||
return isBefore(date, new Date(activity.date));
|
||||
})
|
||||
.map(({ fee, SymbolProfile }) => {
|
||||
@ -1458,19 +1477,37 @@ export class PortfolioService {
|
||||
};
|
||||
}
|
||||
|
||||
private getItems(orders: OrderWithAccount[], date = new Date(0)) {
|
||||
return orders
|
||||
.filter((order) => {
|
||||
// Filter out all orders before given date and type item
|
||||
private getItems(activities: OrderWithAccount[], date = new Date(0)) {
|
||||
return activities
|
||||
.filter((activity) => {
|
||||
// Filter out all activities before given date (drafts) and type item
|
||||
return (
|
||||
isBefore(date, new Date(order.date)) &&
|
||||
order.type === TypeOfOrder.ITEM
|
||||
isBefore(date, new Date(activity.date)) &&
|
||||
activity.type === TypeOfOrder.ITEM
|
||||
);
|
||||
})
|
||||
.map((order) => {
|
||||
.map(({ quantity, SymbolProfile, unitPrice }) => {
|
||||
return this.exchangeRateDataService.toCurrency(
|
||||
new Big(order.quantity).mul(order.unitPrice).toNumber(),
|
||||
order.SymbolProfile.currency,
|
||||
new Big(quantity).mul(unitPrice).toNumber(),
|
||||
SymbolProfile.currency,
|
||||
this.request.user.Settings.settings.baseCurrency
|
||||
);
|
||||
})
|
||||
.reduce(
|
||||
(previous, current) => new Big(previous).plus(current),
|
||||
new Big(0)
|
||||
);
|
||||
}
|
||||
|
||||
private getLiabilities(activities: OrderWithAccount[]) {
|
||||
return activities
|
||||
.filter(({ type }) => {
|
||||
return type === TypeOfOrder.LIABILITY;
|
||||
})
|
||||
.map(({ quantity, SymbolProfile, unitPrice }) => {
|
||||
return this.exchangeRateDataService.toCurrency(
|
||||
new Big(quantity).mul(unitPrice).toNumber(),
|
||||
SymbolProfile.currency,
|
||||
this.request.user.Settings.settings.baseCurrency
|
||||
);
|
||||
})
|
||||
@ -1510,6 +1547,28 @@ export class PortfolioService {
|
||||
return portfolioStart;
|
||||
}
|
||||
|
||||
private getStreaks({
|
||||
investments,
|
||||
savingsRate
|
||||
}: {
|
||||
investments: InvestmentItem[];
|
||||
savingsRate: number;
|
||||
}) {
|
||||
let currentStreak = 0;
|
||||
let longestStreak = 0;
|
||||
|
||||
for (const { investment } of investments) {
|
||||
if (investment >= savingsRate) {
|
||||
currentStreak++;
|
||||
longestStreak = Math.max(longestStreak, currentStreak);
|
||||
} else {
|
||||
currentStreak = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return { currentStreak, longestStreak };
|
||||
}
|
||||
|
||||
private async getSummary({
|
||||
balanceInBaseCurrency,
|
||||
emergencyFundPositionsValueInBaseCurrency,
|
||||
@ -1559,6 +1618,7 @@ export class PortfolioService {
|
||||
const fees = this.getFees({ activities, userCurrency }).toNumber();
|
||||
const firstOrderDate = activities[0]?.date;
|
||||
const items = this.getItems(activities).toNumber();
|
||||
const liabilities = this.getLiabilities(activities).toNumber();
|
||||
|
||||
const totalBuy = this.getTotalByType(activities, userCurrency, 'BUY');
|
||||
const totalSell = this.getTotalByType(activities, userCurrency, 'SELL');
|
||||
@ -1591,6 +1651,7 @@ export class PortfolioService {
|
||||
.plus(performanceInformation.performance.currentValue)
|
||||
.plus(items)
|
||||
.plus(excludedAccountsAndActivities)
|
||||
.minus(liabilities)
|
||||
.toNumber();
|
||||
|
||||
const daysInMarket = differenceInDays(new Date(), firstOrderDate);
|
||||
@ -1617,6 +1678,7 @@ export class PortfolioService {
|
||||
fees,
|
||||
firstOrderDate,
|
||||
items,
|
||||
liabilities,
|
||||
netWorth,
|
||||
totalBuy,
|
||||
totalSell,
|
||||
@ -1841,13 +1903,6 @@ export class PortfolioService {
|
||||
return { accounts, platforms };
|
||||
}
|
||||
|
||||
private async getUserId(aImpersonationId: string, aUserId: string) {
|
||||
const impersonationUserId =
|
||||
await this.impersonationService.validateImpersonationId(aImpersonationId);
|
||||
|
||||
return impersonationUserId || aUserId;
|
||||
}
|
||||
|
||||
private getTotalByType(
|
||||
orders: OrderWithAccount[],
|
||||
currency: string,
|
||||
@ -1874,4 +1929,11 @@ export class PortfolioService {
|
||||
this.baseCurrency
|
||||
);
|
||||
}
|
||||
|
||||
private async getUserId(aImpersonationId: string, aUserId: string) {
|
||||
const impersonationUserId =
|
||||
await this.impersonationService.validateImpersonationId(aImpersonationId);
|
||||
|
||||
return impersonationUserId || aUserId;
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,6 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||
import {
|
||||
DEFAULT_LANGUAGE_CODE,
|
||||
PROPERTY_STRIPE_CONFIG
|
||||
} from '@ghostfolio/common/config';
|
||||
import { Subscription as SubscriptionInterface } from '@ghostfolio/common/interfaces';
|
||||
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
|
||||
import { UserWithSettings } from '@ghostfolio/common/types';
|
||||
import { SubscriptionType } from '@ghostfolio/common/types/subscription-type.type';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
@ -101,19 +97,8 @@ export class SubscriptionService {
|
||||
aCheckoutSessionId
|
||||
);
|
||||
|
||||
let subscriptions: SubscriptionInterface[] = [];
|
||||
|
||||
const stripeConfig = (await this.prismaService.property.findUnique({
|
||||
where: { key: PROPERTY_STRIPE_CONFIG }
|
||||
})) ?? { value: '{}' };
|
||||
|
||||
subscriptions = [JSON.parse(stripeConfig.value)];
|
||||
|
||||
const coupon = subscriptions[0]?.coupon ?? 0;
|
||||
const price = subscriptions[0]?.price ?? 0;
|
||||
|
||||
await this.createSubscription({
|
||||
price: price - coupon,
|
||||
price: session.amount_total / 100,
|
||||
userId: session.client_reference_id
|
||||
});
|
||||
|
||||
|
@ -60,7 +60,7 @@ export class SymbolController {
|
||||
public async getSymbolData(
|
||||
@Param('dataSource') dataSource: DataSource,
|
||||
@Param('symbol') symbol: string,
|
||||
@Query('includeHistoricalData') includeHistoricalData?: number
|
||||
@Query('includeHistoricalData') includeHistoricalData = 0
|
||||
): Promise<SymbolItem> {
|
||||
if (!DataSource[dataSource]) {
|
||||
throw new HttpException(
|
||||
|
@ -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({
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -16,9 +16,11 @@ export function hasNotDefinedValuesInObject(aObject: Object): boolean {
|
||||
export function nullifyValuesInObject<T>(aObject: T, keys: string[]): T {
|
||||
const object = cloneDeep(aObject);
|
||||
|
||||
keys.forEach((key) => {
|
||||
object[key] = null;
|
||||
});
|
||||
if (object) {
|
||||
keys.forEach((key) => {
|
||||
object[key] = null;
|
||||
});
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -186,28 +186,42 @@ export class ExchangeRateDataService {
|
||||
factor = marketData?.marketPrice;
|
||||
} else {
|
||||
// Calculate indirectly via base currency
|
||||
try {
|
||||
const [
|
||||
{ marketPrice: marketPriceBaseCurrencyFromCurrency },
|
||||
{ marketPrice: marketPriceBaseCurrencyToCurrency }
|
||||
] = await Promise.all([
|
||||
this.marketDataService.get({
|
||||
dataSource,
|
||||
date: aDate,
|
||||
symbol: `${this.baseCurrency}${aFromCurrency}`
|
||||
}),
|
||||
this.marketDataService.get({
|
||||
dataSource,
|
||||
date: aDate,
|
||||
symbol: `${this.baseCurrency}${aToCurrency}`
|
||||
})
|
||||
]);
|
||||
|
||||
// Calculate the opposite direction
|
||||
factor =
|
||||
(1 / marketPriceBaseCurrencyFromCurrency) *
|
||||
marketPriceBaseCurrencyToCurrency;
|
||||
let marketPriceBaseCurrencyFromCurrency: number;
|
||||
let marketPriceBaseCurrencyToCurrency: number;
|
||||
|
||||
try {
|
||||
if (this.baseCurrency === aFromCurrency) {
|
||||
marketPriceBaseCurrencyFromCurrency = 1;
|
||||
} else {
|
||||
marketPriceBaseCurrencyFromCurrency = (
|
||||
await this.marketDataService.get({
|
||||
dataSource,
|
||||
date: aDate,
|
||||
symbol: `${this.baseCurrency}${aFromCurrency}`
|
||||
})
|
||||
)?.marketPrice;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
if (this.baseCurrency === aToCurrency) {
|
||||
marketPriceBaseCurrencyToCurrency = 1;
|
||||
} else {
|
||||
marketPriceBaseCurrencyToCurrency = (
|
||||
await this.marketDataService.get({
|
||||
dataSource,
|
||||
date: aDate,
|
||||
symbol: `${this.baseCurrency}${aToCurrency}`
|
||||
})
|
||||
)?.marketPrice;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Calculate the opposite direction
|
||||
factor =
|
||||
(1 / marketPriceBaseCurrencyFromCurrency) *
|
||||
marketPriceBaseCurrencyToCurrency;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -12,22 +12,36 @@ export class ImpersonationService {
|
||||
) {}
|
||||
|
||||
public async validateImpersonationId(aId = '') {
|
||||
const accessObject = await this.prismaService.access.findFirst({
|
||||
where: {
|
||||
GranteeUser: { id: this.request.user.id },
|
||||
id: aId
|
||||
}
|
||||
});
|
||||
if (this.request.user) {
|
||||
const accessObject = await this.prismaService.access.findFirst({
|
||||
where: {
|
||||
GranteeUser: { id: this.request.user.id },
|
||||
id: aId
|
||||
}
|
||||
});
|
||||
|
||||
if (accessObject?.userId) {
|
||||
return accessObject?.userId;
|
||||
} else if (
|
||||
hasPermission(
|
||||
this.request.user.permissions,
|
||||
permissions.impersonateAllUsers
|
||||
)
|
||||
) {
|
||||
return aId;
|
||||
if (accessObject?.userId) {
|
||||
return accessObject.userId;
|
||||
} else if (
|
||||
hasPermission(
|
||||
this.request.user.permissions,
|
||||
permissions.impersonateAllUsers
|
||||
)
|
||||
) {
|
||||
return aId;
|
||||
}
|
||||
} else {
|
||||
// Public access
|
||||
const accessObject = await this.prismaService.access.findFirst({
|
||||
where: {
|
||||
GranteeUser: null,
|
||||
User: { id: aId }
|
||||
}
|
||||
});
|
||||
|
||||
if (accessObject?.userId) {
|
||||
return accessObject.userId;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -5,25 +5,19 @@ 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',
|
||||
'sobre',
|
||||
'ueber-uns'
|
||||
].map((path) => ({
|
||||
path,
|
||||
loadChildren: () =>
|
||||
import('./pages/about/about-page.module').then((m) => m.AboutPageModule)
|
||||
},
|
||||
{
|
||||
path: 'about/changelog',
|
||||
loadChildren: () =>
|
||||
import('./pages/about/changelog/changelog-page.module').then(
|
||||
(m) => m.ChangelogPageModule
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'about/privacy-policy',
|
||||
loadChildren: () =>
|
||||
import('./pages/about/privacy-policy/privacy-policy-page.module').then(
|
||||
(m) => m.PrivacyPolicyPageModule
|
||||
)
|
||||
},
|
||||
})),
|
||||
{
|
||||
path: 'account',
|
||||
loadChildren: () =>
|
||||
@ -48,11 +42,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: () =>
|
||||
@ -149,30 +143,54 @@ const routes: Routes = [
|
||||
loadChildren: () =>
|
||||
import('./pages/demo/demo-page.module').then((m) => m.DemoPageModule)
|
||||
},
|
||||
{
|
||||
path: 'faq',
|
||||
...[
|
||||
'faq',
|
||||
/////
|
||||
'domande-piu-frequenti',
|
||||
'foire-aux-questions',
|
||||
'haeufig-gestellte-fragen',
|
||||
'perguntas-mais-frequentes',
|
||||
'preguntas-mas-frecuentes',
|
||||
'vaak-gestelde-vragen'
|
||||
].map((path) => ({
|
||||
path,
|
||||
loadChildren: () =>
|
||||
import('./pages/faq/faq-page.module').then((m) => m.FaqPageModule)
|
||||
},
|
||||
{
|
||||
path: 'features',
|
||||
})),
|
||||
...[
|
||||
'features',
|
||||
/////
|
||||
'fonctionnalites',
|
||||
'funcionalidades',
|
||||
'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',
|
||||
'mercados',
|
||||
'mercati'
|
||||
].map((path) => ({
|
||||
path,
|
||||
loadChildren: () =>
|
||||
import('./pages/markets/markets-page.module').then(
|
||||
(m) => m.MarketsPageModule
|
||||
)
|
||||
},
|
||||
})),
|
||||
{
|
||||
path: 'open',
|
||||
loadChildren: () =>
|
||||
@ -192,27 +210,53 @@ const routes: Routes = [
|
||||
(m) => m.PortfolioPageModule
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'pricing',
|
||||
...[
|
||||
'pricing',
|
||||
/////
|
||||
'precios',
|
||||
'precos',
|
||||
'preise',
|
||||
'prezzi',
|
||||
'prijzen',
|
||||
'prix'
|
||||
].map((path) => ({
|
||||
path,
|
||||
loadChildren: () =>
|
||||
import('./pages/pricing/pricing-page.module').then(
|
||||
(m) => m.PricingPageModule
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'register',
|
||||
})),
|
||||
...[
|
||||
'register',
|
||||
/////
|
||||
'enregistrement',
|
||||
'iscrizione',
|
||||
'registo',
|
||||
'registratie',
|
||||
'registrierung',
|
||||
'registro'
|
||||
].map((path) => ({
|
||||
path,
|
||||
loadChildren: () =>
|
||||
import('./pages/register/register-page.module').then(
|
||||
(m) => m.RegisterPageModule
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'resources',
|
||||
})),
|
||||
...[
|
||||
'resources',
|
||||
/////
|
||||
'bronnen',
|
||||
'recursos',
|
||||
'ressourcen',
|
||||
'ressources',
|
||||
'risorse'
|
||||
].map((path) => ({
|
||||
path,
|
||||
loadChildren: () =>
|
||||
import('./pages/resources/resources-page.module').then(
|
||||
(m) => m.ResourcesPageModule
|
||||
)
|
||||
},
|
||||
})),
|
||||
{
|
||||
path: 'start',
|
||||
loadChildren: () =>
|
||||
|
@ -44,19 +44,133 @@
|
||||
</main>
|
||||
|
||||
<footer
|
||||
*ngIf="currentRoute === 'start'"
|
||||
class="footer d-flex justify-content-center w-100"
|
||||
*ngIf="
|
||||
(currentRoute === 'blog' ||
|
||||
currentRoute === 'faq' ||
|
||||
currentRoute === 'features' ||
|
||||
currentRoute === 'markets' ||
|
||||
currentRoute === 'open' ||
|
||||
currentRoute === 'pricing' ||
|
||||
currentRoute === 'resources' ||
|
||||
currentRoute === 'register' ||
|
||||
currentRoute === 'start') &&
|
||||
deviceType !== 'mobile'
|
||||
"
|
||||
class="d-flex justify-content-center py-4 w-100"
|
||||
>
|
||||
<div class="container text-center">
|
||||
<div>
|
||||
© 2021 - {{ currentYear }} <a href="https://ghostfol.io">Ghostfolio</a>
|
||||
{{ version }}
|
||||
<div class="container">
|
||||
<div class="mb-3 row">
|
||||
<div class="col-sm">
|
||||
<a [routerLink]="['/']"><gf-logo /></a>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="h6 mt-2" i18n>Personal Finance</div>
|
||||
<ul class="list-unstyled">
|
||||
<li *ngIf="hasPermissionToAccessFearAndGreedIndex">
|
||||
<a i18n [routerLink]="['/markets']">Markets</a>
|
||||
</li>
|
||||
<li><a i18n [routerLink]="['/resources']">Resources</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="h6 mt-2">Ghostfolio</div>
|
||||
<ul class="list-unstyled">
|
||||
<li><a i18n [routerLink]="['/about']">About</a></li>
|
||||
<li *ngIf="hasPermissionForBlog">
|
||||
<a i18n [routerLink]="['/blog']">Blog</a>
|
||||
</li>
|
||||
<li>
|
||||
<a i18n [routerLink]="['/about', 'changelog']">Changelog</a>
|
||||
</li>
|
||||
<li><a i18n [routerLink]="['/features']">Features</a></li>
|
||||
<li *ngIf="hasPermissionForSubscription">
|
||||
<a i18n [routerLink]="['/faq']">Frequently Asked Questions (FAQ)</a>
|
||||
</li>
|
||||
<li>
|
||||
<a i18n [routerLink]="['/about', 'license']">License</a>
|
||||
</li>
|
||||
<li *ngIf="hasPermissionForSubscription">
|
||||
<a [routerLink]="['/open']">Open Startup</a>
|
||||
</li>
|
||||
<li *ngIf="hasPermissionForSubscription">
|
||||
<a i18n [routerLink]="['/pricing']">Pricing</a>
|
||||
</li>
|
||||
<li *ngIf="hasPermissionForSubscription">
|
||||
<a i18n [routerLink]="['/about', 'privacy-policy']"
|
||||
>Privacy Policy</a
|
||||
>
|
||||
</li>
|
||||
<li *ngIf="hasPermissionForSubscription">
|
||||
<a href="https://status.ghostfol.io" title="Ghostfolio Status"
|
||||
>Status</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="h6 mt-2" i18n>Community</div>
|
||||
<ul class="list-unstyled">
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio"
|
||||
title="Find Ghostfolio on GitHub"
|
||||
>GitHub</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
title="Join the Ghostfolio Slack community"
|
||||
>Slack</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://twitter.com/ghostfolio_"
|
||||
title="Follow Ghostfolio on Twitter"
|
||||
>Twitter</a
|
||||
>
|
||||
</li>
|
||||
<li> </li>
|
||||
<li>
|
||||
<a href="../de" title="Ghostfolio in Deutsch">Deutsch</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="../en" title="Ghostfolio in English">English</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="../es" title="Ghostfolio in Español">Español</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="../fr" title="Ghostfolio en Français">Français</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="../it" title="Ghostfolio in Italiano">Italiano</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="../nl" title="Ghostfolio in Nederlands">Nederlands</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="../pt" title="Ghostfolio in Português">Português</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-2 text-muted">
|
||||
<small i18n
|
||||
>The risk of loss in trading can be substantial. It is not advisable to
|
||||
invest money you may need in the short term.</small
|
||||
>
|
||||
|
||||
<div class="row text-center">
|
||||
<div class="col">
|
||||
© 2021 - {{ currentYear }} <a href="https://ghostfol.io">Ghostfolio</a>
|
||||
{{ version }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row text-center text-muted">
|
||||
<div class="col">
|
||||
<small i18n
|
||||
>The risk of loss in trading can be substantial. It is not advisable
|
||||
to invest money you may need in the short term.</small
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
@ -4,6 +4,11 @@
|
||||
display: block;
|
||||
min-height: 100vh;
|
||||
|
||||
footer {
|
||||
background-color: rgba(var(--palette-foreground-text), 0.05);
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
main {
|
||||
min-height: 100vh;
|
||||
padding-top: 5rem;
|
||||
@ -25,14 +30,13 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
height: 5rem;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
footer {
|
||||
background-color: rgba(var(--palette-foreground-text-dark), 0.05);
|
||||
}
|
||||
|
||||
main {
|
||||
.info-message-container {
|
||||
.info-message {
|
||||
|
@ -32,6 +32,9 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
public currentRoute: string;
|
||||
public currentYear = new Date().getFullYear();
|
||||
public deviceType: string;
|
||||
public hasPermissionForBlog: boolean;
|
||||
public hasPermissionForSubscription: boolean;
|
||||
public hasPermissionToAccessFearAndGreedIndex: boolean;
|
||||
public info: InfoItem;
|
||||
public pageTitle: string;
|
||||
public user: User;
|
||||
@ -55,6 +58,22 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
|
||||
public ngOnInit() {
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
this.info = this.dataService.fetchInfo();
|
||||
|
||||
this.hasPermissionForBlog = hasPermission(
|
||||
this.info?.globalPermissions,
|
||||
permissions.enableBlog
|
||||
);
|
||||
|
||||
this.hasPermissionForSubscription = hasPermission(
|
||||
this.info?.globalPermissions,
|
||||
permissions.enableSubscription
|
||||
);
|
||||
|
||||
this.hasPermissionToAccessFearAndGreedIndex = hasPermission(
|
||||
this.info?.globalPermissions,
|
||||
permissions.enableFearAndGreedIndex
|
||||
);
|
||||
|
||||
this.router.events
|
||||
.pipe(filter((event) => event instanceof NavigationEnd))
|
||||
@ -64,8 +83,6 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
const urlSegments = urlSegmentGroup.segments;
|
||||
this.currentRoute = urlSegments[0].path;
|
||||
|
||||
this.info = this.dataService.fetchInfo();
|
||||
|
||||
if (this.deviceType === 'mobile') {
|
||||
setTimeout(() => {
|
||||
const index = this.title.getTitle().indexOf('–');
|
||||
|
@ -14,6 +14,7 @@ import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ServiceWorkerModule } from '@angular/service-worker';
|
||||
import { GfLogoModule } from '@ghostfolio/ui/logo';
|
||||
import { MarkdownModule } from 'ngx-markdown';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
import { NgxStripeModule, STRIPE_PUBLISHABLE_KEY } from 'ngx-stripe';
|
||||
@ -40,6 +41,7 @@ export function NgxStripeFactory(): string {
|
||||
BrowserAnimationsModule,
|
||||
BrowserModule,
|
||||
GfHeaderModule,
|
||||
GfLogoModule,
|
||||
GfSubscriptionInterstitialDialogModule,
|
||||
HttpClientModule,
|
||||
MarkdownModule.forRoot(),
|
||||
|
@ -47,7 +47,7 @@
|
||||
[matMenuTriggerFor]="transactionMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||
<ion-icon name="ellipsis-horizontal"></ion-icon>
|
||||
</button>
|
||||
<mat-menu #transactionMenu="matMenu" xPosition="before">
|
||||
<button mat-menu-item (click)="onDeleteAccess(element.id)">
|
||||
@ -57,6 +57,6 @@
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
||||
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
||||
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
|
||||
</table>
|
||||
|
@ -12,8 +12,9 @@
|
||||
<div class="col-12 d-flex justify-content-center mb-3">
|
||||
<gf-value
|
||||
size="large"
|
||||
[currency]="user?.settings?.baseCurrency"
|
||||
[isCurrency]="true"
|
||||
[locale]="user?.settings?.locale"
|
||||
[unit]="user?.settings?.baseCurrency"
|
||||
[value]="valueInBaseCurrency"
|
||||
></gf-value>
|
||||
</div>
|
||||
@ -24,8 +25,9 @@
|
||||
<gf-value
|
||||
i18n
|
||||
size="medium"
|
||||
[currency]="currency"
|
||||
[isCurrency]="true"
|
||||
[locale]="user?.settings?.locale"
|
||||
[unit]="currency"
|
||||
[value]="balance"
|
||||
>Cash Balance</gf-value
|
||||
>
|
||||
@ -34,8 +36,9 @@
|
||||
<gf-value
|
||||
i18n
|
||||
size="medium"
|
||||
[currency]="currency"
|
||||
[isCurrency]="true"
|
||||
[locale]="user?.settings?.locale"
|
||||
[unit]="currency"
|
||||
[value]="equity"
|
||||
>Equity</gf-value
|
||||
>
|
||||
|
@ -207,6 +207,30 @@
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="comment">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="d-none d-lg-table-cell px-1"
|
||||
mat-header-cell
|
||||
></th>
|
||||
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
|
||||
<button
|
||||
*ngIf="element.comment"
|
||||
class="mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
title="Note"
|
||||
(click)="onOpenComment(element.comment); $event.stopPropagation()"
|
||||
>
|
||||
<ion-icon name="document-text-outline"></ion-icon>
|
||||
</button>
|
||||
</td>
|
||||
<td
|
||||
*matFooterCellDef
|
||||
class="d-none d-lg-table-cell px-1"
|
||||
mat-footer-cell
|
||||
></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th *matHeaderCellDef class="px-1 text-center" i18n mat-header-cell></th>
|
||||
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
||||
@ -216,7 +240,7 @@
|
||||
[matMenuTriggerFor]="accountMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||
<ion-icon name="ellipsis-horizontal"></ion-icon>
|
||||
</button>
|
||||
<mat-menu #accountMenu="matMenu" xPosition="before">
|
||||
<button mat-menu-item (click)="onUpdateAccount(element)">
|
||||
|
@ -58,7 +58,8 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
|
||||
'balance',
|
||||
'value',
|
||||
'currency',
|
||||
'valueInBaseCurrency'
|
||||
'valueInBaseCurrency',
|
||||
'comment'
|
||||
];
|
||||
|
||||
if (this.showActions) {
|
||||
@ -92,6 +93,10 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
public onOpenComment(aComment: string) {
|
||||
alert(aComment);
|
||||
}
|
||||
|
||||
public onUpdateAccount(aAccount: AccountModel) {
|
||||
this.accountToUpdate.emit(aAccount);
|
||||
}
|
||||
|
@ -108,7 +108,7 @@
|
||||
[matMenuTriggerFor]="jobActionsMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||
<ion-icon name="ellipsis-horizontal"></ion-icon>
|
||||
</button>
|
||||
<mat-menu #jobActionsMenu="matMenu" xPosition="before">
|
||||
<button mat-menu-item (click)="onViewData(job.data)">
|
||||
|
@ -140,21 +140,9 @@
|
||||
[matMenuTriggerFor]="assetProfileActionsMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||
<ion-icon name="ellipsis-horizontal"></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"
|
||||
|
@ -82,7 +82,7 @@
|
||||
[matMenuTriggerFor]="platformMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||
<ion-icon name="ellipsis-horizontal"></ion-icon>
|
||||
</button>
|
||||
<mat-menu #platformMenu="matMenu" xPosition="before">
|
||||
<button mat-menu-item (click)="onUpdatePlatform(element)">
|
||||
|
@ -109,7 +109,7 @@
|
||||
[matMenuTriggerFor]="userMenu"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||
<ion-icon name="ellipsis-horizontal"></ion-icon>
|
||||
</button>
|
||||
<mat-menu #userMenu="matMenu" xPosition="before">
|
||||
<button
|
||||
|
@ -1,6 +1,8 @@
|
||||
<div class="mb-2 row">
|
||||
<div class="col-md-6 col-xs-12 d-flex">
|
||||
<div class="align-items-center d-flex flex-grow-1 h5 mb-0 text-truncate">
|
||||
<div
|
||||
class="align-items-center d-flex flex-grow-1 h5 mb-0 py-2 text-truncate"
|
||||
>
|
||||
<span i18n>Performance</span>
|
||||
<gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
@ -13,7 +15,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 +29,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 {}
|
||||
|
@ -8,216 +8,238 @@
|
||||
<gf-logo [label]="pageTitle"></gf-logo>
|
||||
</a>
|
||||
<span class="spacer"></span>
|
||||
<a
|
||||
class="d-none d-sm-block"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'home' || currentRoute === 'zen',
|
||||
'text-decoration-underline':
|
||||
currentRoute === 'home' || currentRoute === 'zen'
|
||||
}"
|
||||
[routerLink]="['/']"
|
||||
>Overview</a
|
||||
>
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'portfolio',
|
||||
'text-decoration-underline': currentRoute === 'portfolio'
|
||||
}"
|
||||
[routerLink]="['/portfolio']"
|
||||
>Portfolio</a
|
||||
>
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'accounts',
|
||||
'text-decoration-underline': currentRoute === 'accounts'
|
||||
}"
|
||||
[routerLink]="['/accounts']"
|
||||
>Accounts</a
|
||||
>
|
||||
<a
|
||||
*ngIf="hasPermissionToAccessAdminControl"
|
||||
class="d-none d-sm-block mx-1"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'admin',
|
||||
'text-decoration-underline': currentRoute === 'admin'
|
||||
}"
|
||||
[routerLink]="['/admin']"
|
||||
>Admin Control</a
|
||||
>
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'resources',
|
||||
'text-decoration-underline': currentRoute === 'resources'
|
||||
}"
|
||||
[routerLink]="['/resources']"
|
||||
>Resources</a
|
||||
>
|
||||
<a
|
||||
*ngIf="
|
||||
hasPermissionForSubscription && user?.subscription?.type === 'Basic'
|
||||
"
|
||||
class="d-none d-sm-block mx-1"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'pricing',
|
||||
'text-decoration-underline': currentRoute === 'pricing'
|
||||
}"
|
||||
[routerLink]="['/pricing']"
|
||||
>Pricing</a
|
||||
>
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'about',
|
||||
'text-decoration-underline': currentRoute === 'about'
|
||||
}"
|
||||
[routerLink]="['/about']"
|
||||
>About</a
|
||||
>
|
||||
<button
|
||||
class="no-min-width px-1"
|
||||
mat-flat-button
|
||||
[matMenuTriggerFor]="accountMenu"
|
||||
(menuClosed)="onMenuClosed()"
|
||||
(menuOpened)="onMenuOpened()"
|
||||
>
|
||||
<ion-icon
|
||||
class="d-none d-sm-block"
|
||||
name="person-circle-outline"
|
||||
size="large"
|
||||
></ion-icon>
|
||||
<ion-icon
|
||||
class="d-block d-sm-none"
|
||||
size="large"
|
||||
[name]="isMenuOpen ? 'close-outline' : 'menu-outline'"
|
||||
></ion-icon>
|
||||
</button>
|
||||
<mat-menu #accountMenu="matMenu" xPosition="before">
|
||||
<ng-container *ngIf="user?.access?.length > 0">
|
||||
<button mat-menu-item (click)="impersonateAccount(null)">
|
||||
<ion-icon
|
||||
*ngIf="user?.access?.length > 0"
|
||||
class="mr-2"
|
||||
[name]="
|
||||
impersonationId
|
||||
? 'radio-button-off-outline'
|
||||
: 'radio-button-on-outline'
|
||||
"
|
||||
></ion-icon>
|
||||
<span i18n>Me</span>
|
||||
</button>
|
||||
<button
|
||||
*ngFor="let accessItem of user?.access"
|
||||
mat-menu-item
|
||||
(click)="impersonateAccount(accessItem.id)"
|
||||
<ul class="alig-items-center d-flex list-inline m-0">
|
||||
<li class="list-inline-item">
|
||||
<a
|
||||
class="d-none d-sm-block"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
'font-weight-bold':
|
||||
currentRoute === 'home' || currentRoute === 'zen',
|
||||
'text-decoration-underline':
|
||||
currentRoute === 'home' || currentRoute === 'zen'
|
||||
}"
|
||||
[routerLink]="['/']"
|
||||
>Overview</a
|
||||
>
|
||||
<ion-icon
|
||||
class="mr-2"
|
||||
name="square-outline"
|
||||
[name]="
|
||||
accessItem.id === impersonationId
|
||||
? 'radio-button-on-outline'
|
||||
: 'radio-button-off-outline'
|
||||
"
|
||||
></ion-icon>
|
||||
<span *ngIf="accessItem.alias">{{ accessItem.alias }}</span>
|
||||
<span *ngIf="!accessItem.alias" i18n>User</span>
|
||||
</button>
|
||||
<hr class="m-0" />
|
||||
</ng-container>
|
||||
<a
|
||||
class="d-flex d-sm-none"
|
||||
i18n
|
||||
mat-menu-item
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'home' || currentRoute === 'zen'
|
||||
}"
|
||||
[routerLink]="['/']"
|
||||
>Overview</a
|
||||
>
|
||||
<a
|
||||
class="d-flex d-sm-none"
|
||||
i18n
|
||||
mat-menu-item
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'portfolio'
|
||||
}"
|
||||
[routerLink]="['/portfolio']"
|
||||
>Portfolio</a
|
||||
>
|
||||
<a
|
||||
class="d-flex d-sm-none"
|
||||
i18n
|
||||
mat-menu-item
|
||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'accounts' }"
|
||||
[routerLink]="['/accounts']"
|
||||
>Accounts</a
|
||||
>
|
||||
<a
|
||||
i18n
|
||||
mat-menu-item
|
||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'account' }"
|
||||
[routerLink]="['/account']"
|
||||
>My Ghostfolio</a
|
||||
>
|
||||
<a
|
||||
*ngIf="hasPermissionToAccessAdminControl"
|
||||
class="d-flex d-sm-none"
|
||||
i18n
|
||||
mat-menu-item
|
||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'admin' }"
|
||||
[routerLink]="['/admin']"
|
||||
>Admin Control</a
|
||||
>
|
||||
<hr class="m-0" />
|
||||
<a
|
||||
class="d-flex d-sm-none"
|
||||
i18n
|
||||
mat-menu-item
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'resources'
|
||||
}"
|
||||
[routerLink]="['/resources']"
|
||||
>Resources</a
|
||||
>
|
||||
<a
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'portfolio',
|
||||
'text-decoration-underline': currentRoute === 'portfolio'
|
||||
}"
|
||||
[routerLink]="['/portfolio']"
|
||||
>Portfolio</a
|
||||
>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'accounts',
|
||||
'text-decoration-underline': currentRoute === 'accounts'
|
||||
}"
|
||||
[routerLink]="['/accounts']"
|
||||
>Accounts</a
|
||||
>
|
||||
</li>
|
||||
<li *ngIf="hasPermissionToAccessAdminControl" class="list-inline-item">
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'admin',
|
||||
'text-decoration-underline': currentRoute === 'admin'
|
||||
}"
|
||||
[routerLink]="['/admin']"
|
||||
>Admin Control</a
|
||||
>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'resources',
|
||||
'text-decoration-underline': currentRoute === 'resources'
|
||||
}"
|
||||
[routerLink]="['/resources']"
|
||||
>Resources</a
|
||||
>
|
||||
</li>
|
||||
<li
|
||||
*ngIf="
|
||||
hasPermissionForSubscription && user?.subscription?.type === 'Basic'
|
||||
"
|
||||
class="d-flex d-sm-none"
|
||||
i18n
|
||||
mat-menu-item
|
||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'pricing' }"
|
||||
[routerLink]="['/pricing']"
|
||||
>Pricing</a
|
||||
class="list-inline-item"
|
||||
>
|
||||
<a
|
||||
class="d-flex d-sm-none"
|
||||
i18n
|
||||
mat-menu-item
|
||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'about' }"
|
||||
[routerLink]="['/about']"
|
||||
>About Ghostfolio</a
|
||||
>
|
||||
<hr class="d-flex d-sm-none m-0" />
|
||||
<button mat-menu-item (click)="onSignOut()">Logout</button>
|
||||
</mat-menu>
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'pricing',
|
||||
'text-decoration-underline': currentRoute === 'pricing'
|
||||
}"
|
||||
[routerLink]="['/pricing']"
|
||||
>Pricing</a
|
||||
>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'about',
|
||||
'text-decoration-underline': currentRoute === 'about'
|
||||
}"
|
||||
[routerLink]="['/about']"
|
||||
>About</a
|
||||
>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<button
|
||||
class="no-min-width px-1"
|
||||
mat-flat-button
|
||||
[matMenuTriggerFor]="accountMenu"
|
||||
(menuClosed)="onMenuClosed()"
|
||||
(menuOpened)="onMenuOpened()"
|
||||
>
|
||||
<ion-icon
|
||||
class="d-none d-sm-block"
|
||||
name="person-circle-outline"
|
||||
size="large"
|
||||
></ion-icon>
|
||||
<ion-icon
|
||||
class="d-block d-sm-none"
|
||||
size="large"
|
||||
[name]="isMenuOpen ? 'close-outline' : 'menu-outline'"
|
||||
></ion-icon>
|
||||
</button>
|
||||
<mat-menu #accountMenu="matMenu" xPosition="before">
|
||||
<ng-container *ngIf="user?.access?.length > 0">
|
||||
<button mat-menu-item (click)="impersonateAccount(null)">
|
||||
<ion-icon
|
||||
*ngIf="user?.access?.length > 0"
|
||||
class="mr-2"
|
||||
[name]="
|
||||
impersonationId
|
||||
? 'radio-button-off-outline'
|
||||
: 'radio-button-on-outline'
|
||||
"
|
||||
></ion-icon>
|
||||
<span i18n>Me</span>
|
||||
</button>
|
||||
<button
|
||||
*ngFor="let accessItem of user?.access"
|
||||
mat-menu-item
|
||||
(click)="impersonateAccount(accessItem.id)"
|
||||
>
|
||||
<ion-icon
|
||||
class="mr-2"
|
||||
name="square-outline"
|
||||
[name]="
|
||||
accessItem.id === impersonationId
|
||||
? 'radio-button-on-outline'
|
||||
: 'radio-button-off-outline'
|
||||
"
|
||||
></ion-icon>
|
||||
<span *ngIf="accessItem.alias">{{ accessItem.alias }}</span>
|
||||
<span *ngIf="!accessItem.alias" i18n>User</span>
|
||||
</button>
|
||||
<hr class="m-0" />
|
||||
</ng-container>
|
||||
<a
|
||||
class="d-flex d-sm-none"
|
||||
i18n
|
||||
mat-menu-item
|
||||
[ngClass]="{
|
||||
'font-weight-bold':
|
||||
currentRoute === 'home' || currentRoute === 'zen'
|
||||
}"
|
||||
[routerLink]="['/']"
|
||||
>Overview</a
|
||||
>
|
||||
<a
|
||||
class="d-flex d-sm-none"
|
||||
i18n
|
||||
mat-menu-item
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'portfolio'
|
||||
}"
|
||||
[routerLink]="['/portfolio']"
|
||||
>Portfolio</a
|
||||
>
|
||||
<a
|
||||
class="d-flex d-sm-none"
|
||||
i18n
|
||||
mat-menu-item
|
||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'accounts' }"
|
||||
[routerLink]="['/accounts']"
|
||||
>Accounts</a
|
||||
>
|
||||
<a
|
||||
i18n
|
||||
mat-menu-item
|
||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'account' }"
|
||||
[routerLink]="['/account']"
|
||||
>My Ghostfolio</a
|
||||
>
|
||||
<a
|
||||
*ngIf="hasPermissionToAccessAdminControl"
|
||||
class="d-flex d-sm-none"
|
||||
i18n
|
||||
mat-menu-item
|
||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'admin' }"
|
||||
[routerLink]="['/admin']"
|
||||
>Admin Control</a
|
||||
>
|
||||
<hr class="m-0" />
|
||||
<a
|
||||
class="d-flex d-sm-none"
|
||||
i18n
|
||||
mat-menu-item
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'resources'
|
||||
}"
|
||||
[routerLink]="['/resources']"
|
||||
>Resources</a
|
||||
>
|
||||
<a
|
||||
*ngIf="
|
||||
hasPermissionForSubscription &&
|
||||
user?.subscription?.type === 'Basic'
|
||||
"
|
||||
class="d-flex d-sm-none"
|
||||
i18n
|
||||
mat-menu-item
|
||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'pricing' }"
|
||||
[routerLink]="['/pricing']"
|
||||
>Pricing</a
|
||||
>
|
||||
<a
|
||||
class="d-flex d-sm-none"
|
||||
i18n
|
||||
mat-menu-item
|
||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'about' }"
|
||||
[routerLink]="['/about']"
|
||||
>About Ghostfolio</a
|
||||
>
|
||||
<hr class="d-flex d-sm-none m-0" />
|
||||
<button mat-menu-item (click)="onSignOut()">Logout</button>
|
||||
</mat-menu>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="user === null">
|
||||
<a
|
||||
@ -231,67 +253,86 @@
|
||||
></gf-logo>
|
||||
</a>
|
||||
<span class="spacer"></span>
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'features',
|
||||
'text-decoration-underline': currentRoute === 'features'
|
||||
}"
|
||||
[routerLink]="['/features']"
|
||||
>Features</a
|
||||
>
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'about',
|
||||
'text-decoration-underline': currentRoute === 'about'
|
||||
}"
|
||||
[routerLink]="['/about']"
|
||||
>About</a
|
||||
>
|
||||
<a
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'pricing',
|
||||
'text-decoration-underline': currentRoute === 'pricing'
|
||||
}"
|
||||
[routerLink]="['/pricing']"
|
||||
>Pricing</a
|
||||
>
|
||||
<a
|
||||
*ngIf="hasPermissionToAccessFearAndGreedIndex"
|
||||
class="d-none d-sm-block mx-1"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'markets',
|
||||
'text-decoration-underline': currentRoute === 'markets'
|
||||
}"
|
||||
[routerLink]="['/markets']"
|
||||
>Markets</a
|
||||
>
|
||||
<a
|
||||
class="d-none d-sm-block no-min-width"
|
||||
href="https://github.com/ghostfolio/ghostfolio"
|
||||
mat-icon-button
|
||||
><ion-icon name="logo-github"></ion-icon
|
||||
></a>
|
||||
<button class="mx-1" mat-flat-button (click)="openLoginDialog()">
|
||||
<ng-container i18n>Sign in</ng-container>
|
||||
</button>
|
||||
<a
|
||||
*ngIf="currentRoute !== 'register' && hasPermissionToCreateUser"
|
||||
class="d-none d-sm-block"
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
[routerLink]="['/register']"
|
||||
><ng-container i18n>Get started</ng-container>
|
||||
</a>
|
||||
<ul class="alig-items-center d-flex list-inline m-0">
|
||||
<li class="list-inline-item">
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'features',
|
||||
'text-decoration-underline': currentRoute === 'features'
|
||||
}"
|
||||
[routerLink]="['/features']"
|
||||
>Features</a
|
||||
>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'about',
|
||||
'text-decoration-underline': currentRoute === 'about'
|
||||
}"
|
||||
[routerLink]="['/about']"
|
||||
>About</a
|
||||
>
|
||||
</li>
|
||||
<li *ngIf="hasPermissionForSubscription" class="list-inline-item">
|
||||
<a
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'pricing',
|
||||
'text-decoration-underline': currentRoute === 'pricing'
|
||||
}"
|
||||
[routerLink]="['/pricing']"
|
||||
>Pricing</a
|
||||
>
|
||||
</li>
|
||||
<li
|
||||
*ngIf="hasPermissionToAccessFearAndGreedIndex"
|
||||
class="list-inline-item"
|
||||
>
|
||||
<a
|
||||
class="d-none d-sm-block mx-1"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[ngClass]="{
|
||||
'font-weight-bold': currentRoute === 'markets',
|
||||
'text-decoration-underline': currentRoute === 'markets'
|
||||
}"
|
||||
[routerLink]="['/markets']"
|
||||
>Markets</a
|
||||
>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<a
|
||||
class="d-none d-sm-block no-min-width p-1"
|
||||
href="https://github.com/ghostfolio/ghostfolio"
|
||||
mat-flat-button
|
||||
><ion-icon name="logo-github"></ion-icon
|
||||
></a>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<button class="mx-1" mat-flat-button (click)="openLoginDialog()">
|
||||
<ng-container i18n>Sign in</ng-container>
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
*ngIf="currentRoute !== 'register' && hasPermissionToCreateUser"
|
||||
class="list-inline-item"
|
||||
>
|
||||
<a
|
||||
class="d-none d-sm-block"
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
[routerLink]="['/register']"
|
||||
><ng-container i18n>Get started</ng-container>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-container>
|
||||
</mat-toolbar>
|
||||
|
@ -13,8 +13,9 @@
|
||||
<div class="justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
[currency]="baseCurrency"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.totalBuy"
|
||||
></gf-value>
|
||||
</div>
|
||||
@ -24,8 +25,9 @@
|
||||
<div class="justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
[currency]="baseCurrency"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.totalSell"
|
||||
></gf-value>
|
||||
</div>
|
||||
@ -38,8 +40,9 @@
|
||||
<div class="justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
[currency]="baseCurrency"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.committedFunds"
|
||||
></gf-value>
|
||||
</div>
|
||||
@ -49,8 +52,9 @@
|
||||
<div class="justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
[currency]="baseCurrency"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.currentGrossPerformance"
|
||||
></gf-value>
|
||||
</div>
|
||||
@ -79,8 +83,9 @@
|
||||
<span *ngIf="summary?.fees || summary?.fees === 0" class="mr-1">-</span>
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
[currency]="baseCurrency"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.fees"
|
||||
></gf-value>
|
||||
</div>
|
||||
@ -93,8 +98,9 @@
|
||||
<div class="justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
[currency]="baseCurrency"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.currentNetPerformance"
|
||||
></gf-value>
|
||||
</div>
|
||||
@ -121,8 +127,9 @@
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
position="end"
|
||||
[currency]="baseCurrency"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.currentValue"
|
||||
></gf-value>
|
||||
</div>
|
||||
@ -132,8 +139,9 @@
|
||||
<div class="justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
[currency]="baseCurrency"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.items"
|
||||
></gf-value>
|
||||
</div>
|
||||
@ -152,8 +160,9 @@
|
||||
></ion-icon>
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
[currency]="baseCurrency"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.emergencyFund"
|
||||
></gf-value>
|
||||
</div>
|
||||
@ -163,8 +172,9 @@
|
||||
<div class="justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
[currency]="baseCurrency"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.cash"
|
||||
></gf-value>
|
||||
</div>
|
||||
@ -174,8 +184,9 @@
|
||||
<div class="justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
[currency]="baseCurrency"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.excludedAccountsAndActivities"
|
||||
></gf-value>
|
||||
</div>
|
||||
@ -183,13 +194,34 @@
|
||||
<div class="row">
|
||||
<div class="col"><hr /></div>
|
||||
</div>
|
||||
<div class="flex-nowrap px-3 py-1 row">
|
||||
<div class="flex-grow-1 text-truncate" i18n>Liabilities</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<span
|
||||
*ngIf="summary?.liabilities || summary?.liabilities === 0"
|
||||
class="mr-1"
|
||||
>-</span
|
||||
>
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.liabilities"
|
||||
></gf-value>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col"><hr /></div>
|
||||
</div>
|
||||
<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"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.netWorth"
|
||||
></gf-value>
|
||||
</div>
|
||||
@ -217,8 +249,9 @@
|
||||
<div class="justify-content-end">
|
||||
<gf-value
|
||||
class="justify-content-end"
|
||||
[currency]="baseCurrency"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="isLoading ? undefined : summary?.dividend"
|
||||
></gf-value>
|
||||
</div>
|
||||
|
@ -12,8 +12,9 @@
|
||||
<div class="col-12 d-flex justify-content-center mb-3">
|
||||
<gf-value
|
||||
size="large"
|
||||
[currency]="data.baseCurrency"
|
||||
[isCurrency]="true"
|
||||
[locale]="data.locale"
|
||||
[unit]="data.baseCurrency"
|
||||
[value]="value"
|
||||
></gf-value>
|
||||
</div>
|
||||
@ -40,8 +41,9 @@
|
||||
i18n
|
||||
size="medium"
|
||||
[colorizeSign]="true"
|
||||
[currency]="data.baseCurrency"
|
||||
[isCurrency]="true"
|
||||
[locale]="data.locale"
|
||||
[unit]="data.baseCurrency"
|
||||
[value]="netPerformance"
|
||||
>Change</gf-value
|
||||
>
|
||||
@ -61,8 +63,9 @@
|
||||
<gf-value
|
||||
i18n
|
||||
size="medium"
|
||||
[currency]="SymbolProfile?.currency"
|
||||
[isCurrency]="true"
|
||||
[locale]="data.locale"
|
||||
[unit]="SymbolProfile?.currency"
|
||||
[value]="averagePrice"
|
||||
>Average Unit Price</gf-value
|
||||
>
|
||||
@ -71,8 +74,9 @@
|
||||
<gf-value
|
||||
i18n
|
||||
size="medium"
|
||||
[currency]="SymbolProfile?.currency"
|
||||
[isCurrency]="true"
|
||||
[locale]="data.locale"
|
||||
[unit]="SymbolProfile?.currency"
|
||||
[value]="marketPrice"
|
||||
>Market Price</gf-value
|
||||
>
|
||||
@ -81,9 +85,10 @@
|
||||
<gf-value
|
||||
i18n
|
||||
size="medium"
|
||||
[currency]="SymbolProfile?.currency"
|
||||
[isCurrency]="true"
|
||||
[locale]="data.locale"
|
||||
[ngClass]="{ 'text-danger': minPrice?.toFixed(2) === marketPrice?.toFixed(2) && maxPrice?.toFixed(2) !== minPrice?.toFixed(2) }"
|
||||
[unit]="SymbolProfile?.currency"
|
||||
[value]="minPrice"
|
||||
>Minimum Price</gf-value
|
||||
>
|
||||
@ -92,9 +97,10 @@
|
||||
<gf-value
|
||||
i18n
|
||||
size="medium"
|
||||
[currency]="SymbolProfile?.currency"
|
||||
[isCurrency]="true"
|
||||
[locale]="data.locale"
|
||||
[ngClass]="{ 'text-success': maxPrice?.toFixed(2) === marketPrice?.toFixed(2) && maxPrice?.toFixed(2) !== minPrice?.toFixed(2) }"
|
||||
[unit]="SymbolProfile?.currency"
|
||||
[value]="maxPrice"
|
||||
>Maximum Price</gf-value
|
||||
>
|
||||
@ -113,8 +119,9 @@
|
||||
<gf-value
|
||||
i18n
|
||||
size="medium"
|
||||
[currency]="data.baseCurrency"
|
||||
[isCurrency]="true"
|
||||
[locale]="data.locale"
|
||||
[unit]="data.baseCurrency"
|
||||
[value]="investment"
|
||||
>Investment</gf-value
|
||||
>
|
||||
@ -123,8 +130,9 @@
|
||||
<gf-value
|
||||
i18n
|
||||
size="medium"
|
||||
[currency]="data.baseCurrency"
|
||||
[isCurrency]="true"
|
||||
[locale]="data.locale"
|
||||
[unit]="data.baseCurrency"
|
||||
[value]="dividendInBaseCurrency"
|
||||
>Dividend</gf-value
|
||||
>
|
||||
@ -133,8 +141,9 @@
|
||||
<gf-value
|
||||
i18n
|
||||
size="medium"
|
||||
[currency]="data.baseCurrency"
|
||||
[isCurrency]="true"
|
||||
[locale]="data.locale"
|
||||
[unit]="data.baseCurrency"
|
||||
[value]="feeInBaseCurrency"
|
||||
>Fees</gf-value
|
||||
>
|
||||
|
@ -45,8 +45,9 @@
|
||||
<gf-value
|
||||
class="mr-3"
|
||||
[colorizeSign]="true"
|
||||
[currency]="baseCurrency"
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[unit]="baseCurrency"
|
||||
[value]="position?.netPerformance"
|
||||
></gf-value>
|
||||
<gf-value
|
||||
|
@ -7,6 +7,54 @@ import { AboutPageComponent } from './about-page.component';
|
||||
const routes: Routes = [
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () =>
|
||||
import('./overview/about-overview-page.module').then(
|
||||
(m) => m.AboutOverviewPageModule
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'changelog',
|
||||
loadChildren: () =>
|
||||
import('./changelog/changelog-page.module').then(
|
||||
(m) => m.ChangelogPageModule
|
||||
)
|
||||
},
|
||||
...[
|
||||
'license',
|
||||
/////
|
||||
'licenca',
|
||||
'licence',
|
||||
'licencia',
|
||||
'licentie',
|
||||
'lizenz',
|
||||
'licenza'
|
||||
].map((path) => ({
|
||||
path,
|
||||
loadChildren: () =>
|
||||
import('./license/license-page.module').then(
|
||||
(m) => m.LicensePageModule
|
||||
)
|
||||
})),
|
||||
...[
|
||||
'privacy-policy',
|
||||
/////
|
||||
'datenschutzbestimmungen',
|
||||
'informativa-sulla-privacy',
|
||||
'politique-de-confidentialite',
|
||||
'politica-de-privacidad',
|
||||
'politica-de-privacidade',
|
||||
'privacybeleid'
|
||||
].map((path) => ({
|
||||
path,
|
||||
loadChildren: () =>
|
||||
import('./privacy-policy/privacy-policy-page.module').then(
|
||||
(m) => m.PrivacyPolicyPageModule
|
||||
)
|
||||
}))
|
||||
],
|
||||
component: AboutPageComponent,
|
||||
path: '',
|
||||
title: $localize`About`
|
||||
|
@ -1,28 +1,31 @@
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { environment } from '@ghostfolio/client/../environments/environment';
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
HostBinding,
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
|
||||
import { Statistics, User } from '@ghostfolio/common/interfaces';
|
||||
import { TabConfiguration, User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
selector: 'gf-about-page',
|
||||
styleUrls: ['./about-page.scss'],
|
||||
templateUrl: './about-page.html'
|
||||
})
|
||||
export class AboutPageComponent implements OnDestroy, OnInit {
|
||||
public defaultLanguageCode = DEFAULT_LANGUAGE_CODE;
|
||||
public hasPermissionForBlog: boolean;
|
||||
public hasPermissionForStatistics: boolean;
|
||||
@HostBinding('class.with-info-message') get getHasMessage() {
|
||||
return this.hasMessage;
|
||||
}
|
||||
|
||||
public hasMessage: boolean;
|
||||
public hasPermissionForSubscription: boolean;
|
||||
public isLoggedIn: boolean;
|
||||
public statistics: Statistics;
|
||||
public tabs: TabConfiguration[] = [];
|
||||
public user: User;
|
||||
public version = environment.version;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
@ -31,38 +34,55 @@ export class AboutPageComponent implements OnDestroy, OnInit {
|
||||
private dataService: DataService,
|
||||
private userService: UserService
|
||||
) {
|
||||
const { globalPermissions, statistics } = this.dataService.fetchInfo();
|
||||
|
||||
this.hasPermissionForBlog = hasPermission(
|
||||
globalPermissions,
|
||||
permissions.enableBlog
|
||||
);
|
||||
|
||||
this.hasPermissionForStatistics = hasPermission(
|
||||
globalPermissions,
|
||||
permissions.enableStatistics
|
||||
);
|
||||
const { globalPermissions, systemMessage } = this.dataService.fetchInfo();
|
||||
|
||||
this.hasPermissionForSubscription = hasPermission(
|
||||
globalPermissions,
|
||||
permissions.enableSubscription
|
||||
);
|
||||
|
||||
this.statistics = statistics;
|
||||
}
|
||||
|
||||
public ngOnInit() {
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((state) => {
|
||||
if (state?.user) {
|
||||
this.tabs = [
|
||||
{
|
||||
iconName: 'reader-outline',
|
||||
label: $localize`About`,
|
||||
path: ['/about']
|
||||
},
|
||||
{
|
||||
iconName: 'sparkles-outline',
|
||||
label: $localize`Changelog`,
|
||||
path: ['/about', 'changelog']
|
||||
},
|
||||
{
|
||||
iconName: 'ribbon-outline',
|
||||
label: $localize`License`,
|
||||
path: ['/about', 'license']
|
||||
},
|
||||
{
|
||||
iconName: 'shield-checkmark-outline',
|
||||
label: $localize`Privacy Policy`,
|
||||
path: ['/about', 'privacy-policy'],
|
||||
showCondition: this.hasPermissionForSubscription
|
||||
}
|
||||
];
|
||||
this.user = state.user;
|
||||
|
||||
this.hasMessage =
|
||||
hasPermission(
|
||||
this.user?.permissions,
|
||||
permissions.createUserAccount
|
||||
) || !!systemMessage;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnInit() {}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
|
@ -1,253 +1,21 @@
|
||||
<div class="container">
|
||||
<div class="mb-5 row">
|
||||
<div class="col">
|
||||
<h3 class="d-none d-sm-block mb-3 text-center">About Ghostfolio</h3>
|
||||
<div class="about-container">
|
||||
<p>
|
||||
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
|
||||
<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
|
||||
>
|
||||
and is driven by the efforts of its
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio/graphs/contributors"
|
||||
title="Contributors to Ghostfolio"
|
||||
>contributors</a
|
||||
>.
|
||||
<ng-container *ngIf="version">
|
||||
This instance is running Ghostfolio {{ version }}.
|
||||
</ng-container>
|
||||
<ng-container *ngIf="hasPermissionForStatistics"
|
||||
>Check the system status at
|
||||
<a href="https://status.ghostfol.io" title="Ghostfolio status"
|
||||
>status.ghostfol.io</a
|
||||
>.</ng-container
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
If you encounter a bug or would like to suggest an improvement or a
|
||||
new
|
||||
<a [routerLink]="['/features']">feature</a>, please join the
|
||||
Ghostfolio
|
||||
<a
|
||||
href="https://ghostfolio.slack.com"
|
||||
title="Join the Ghostfolio Slack community"
|
||||
>Slack community</a
|
||||
>, tweet to
|
||||
<a
|
||||
href="https://twitter.com/ghostfolio_"
|
||||
title="Tweet to Ghostfolio on Twitter"
|
||||
>@ghostfolio_</a
|
||||
><ng-container *ngIf="user?.subscription?.type === 'Premium'"
|
||||
>, send an e-mail to
|
||||
<a href="mailto:hi@ghostfol.io" title="Send an e-mail"
|
||||
>hi@ghostfol.io</a
|
||||
></ng-container
|
||||
>
|
||||
or start a discussion at
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio"
|
||||
title="Find Ghostfolio on GitHub"
|
||||
>GitHub</a
|
||||
>.
|
||||
</p>
|
||||
<p class="text-center">
|
||||
<a
|
||||
class="mx-2"
|
||||
href="https://twitter.com/ghostfolio_"
|
||||
mat-icon-button
|
||||
title="Follow Ghostfolio on Twitter"
|
||||
>
|
||||
<ion-icon name="logo-twitter"></ion-icon>
|
||||
</a>
|
||||
<a
|
||||
*ngIf="user?.subscription?.type === 'Premium'"
|
||||
class="mx-2"
|
||||
href="mailto:hi@ghostfol.io"
|
||||
mat-icon-button
|
||||
title="Send an e-mail"
|
||||
>
|
||||
<ion-icon name="mail"></ion-icon>
|
||||
</a>
|
||||
<a
|
||||
class="mx-2"
|
||||
href="https://ghostfolio.slack.com"
|
||||
mat-icon-button
|
||||
title="Join the Ghostfolio Slack channel"
|
||||
>
|
||||
<ion-icon name="logo-slack"></ion-icon>
|
||||
</a>
|
||||
<a
|
||||
class="mx-2"
|
||||
href="https://github.com/ghostfolio/ghostfolio"
|
||||
mat-icon-button
|
||||
title="Find Ghostfolio on GitHub"
|
||||
>
|
||||
<ion-icon name="logo-github"></ion-icon>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
class="d-flex justify-content-center"
|
||||
>
|
||||
<div
|
||||
class="independent-and-bootstrapped-logo mb-2"
|
||||
title="Ghostfolio is an independent & bootstrapped business"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
*ngIf="!hasPermissionForSubscription"
|
||||
class="d-flex justify-content-center"
|
||||
>
|
||||
<a
|
||||
href="https://www.buymeacoffee.com/ghostfolio"
|
||||
target="_blank"
|
||||
title="Support Ghostfolio"
|
||||
><img
|
||||
class="mb-2"
|
||||
src="../assets/images/button-buy-me-a-coffee.png"
|
||||
width="180"
|
||||
/></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<mat-tab-nav-panel #tabPanel class="flex-grow-1 overflow-auto">
|
||||
<router-outlet></router-outlet>
|
||||
</mat-tab-nav-panel>
|
||||
|
||||
<div *ngIf="hasPermissionForStatistics" class="mb-5 row">
|
||||
<div class="col">
|
||||
<h3 class="mb-3 text-center">Ghostfolio in Numbers</h3>
|
||||
<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>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div *ngIf="hasPermissionForSubscription" class="col-md-3 col-xs-12 my-2">
|
||||
<a
|
||||
class="py-4 w-100"
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
[routerLink]="['/faq']"
|
||||
>FAQ</a
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="col-md-3 col-xs-12 my-2"
|
||||
[ngClass]="{ 'offset-md-4': !hasPermissionForBlog }"
|
||||
<nav mat-align-tabs="center" mat-tab-nav-bar [tabPanel]="tabPanel">
|
||||
<ng-container *ngFor="let tab of tabs">
|
||||
<a
|
||||
#rla="routerLinkActive"
|
||||
*ngIf="tab.showCondition !== false"
|
||||
class="px-3"
|
||||
mat-tab-link
|
||||
routerLinkActive
|
||||
[active]="rla.isActive"
|
||||
[routerLink]="tab.path"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
>
|
||||
<a
|
||||
class="py-4 w-100"
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
[routerLink]="['/about', 'changelog']"
|
||||
>Changelog & License</a
|
||||
>
|
||||
</div>
|
||||
<div *ngIf="hasPermissionForSubscription" class="col-md-3 col-xs-12 my-2">
|
||||
<a
|
||||
class="py-4 w-100"
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
[routerLink]="['/about', 'privacy-policy']"
|
||||
>Privacy Policy</a
|
||||
>
|
||||
</div>
|
||||
<div *ngIf="hasPermissionForBlog" class="col-md-3 col-xs-12 my-2">
|
||||
<a
|
||||
class="py-4 w-100"
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
[routerLink]="['/blog']"
|
||||
>Blog</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ion-icon size="large" [name]="tab.iconName"></ion-icon>
|
||||
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
|
||||
</a>
|
||||
</ng-container>
|
||||
</nav>
|
||||
|
@ -1,21 +1,14 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { AboutPageRoutingModule } from './about-page-routing.module';
|
||||
import { AboutPageComponent } from './about-page.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AboutPageComponent],
|
||||
imports: [
|
||||
AboutPageRoutingModule,
|
||||
CommonModule,
|
||||
GfValueModule,
|
||||
MatButtonModule,
|
||||
MatCardModule
|
||||
],
|
||||
imports: [CommonModule, MatTabsModule, AboutPageRoutingModule, RouterModule],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class AboutPageModule {}
|
||||
|
@ -1,36 +1,35 @@
|
||||
@import 'apps/client/src/styles/ghostfolio-style';
|
||||
|
||||
:host {
|
||||
color: rgb(var(--dark-primary-text));
|
||||
display: block;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 5rem);
|
||||
overflow-y: auto;
|
||||
|
||||
.about-container {
|
||||
a {
|
||||
color: rgba(var(--palette-primary-500), 1);
|
||||
font-weight: 500;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
|
||||
&:hover {
|
||||
color: rgba(var(--palette-primary-300), 1);
|
||||
}
|
||||
::ng-deep {
|
||||
gf-about-page,
|
||||
gf-changelog-page,
|
||||
gf-privacy-policy-page {
|
||||
flex: 1 1 auto;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.independent-and-bootstrapped-logo {
|
||||
background-image: url('/assets/bootstrapped-dark.svg');
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
height: 2rem;
|
||||
opacity: 0.87;
|
||||
width: 10rem;
|
||||
.mat-mdc-tab-link-container {
|
||||
--mdc-tab-indicator-active-indicator-color: transparent;
|
||||
|
||||
.mat-mdc-tab-link {
|
||||
&:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
color: rgb(var(--light-primary-text));
|
||||
|
||||
.about-container {
|
||||
.independent-and-bootstrapped-logo {
|
||||
background-image: url('/assets/bootstrapped-light.svg');
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ const routes: Routes = [
|
||||
canActivate: [AuthGuard],
|
||||
component: ChangelogPageComponent,
|
||||
path: '',
|
||||
title: $localize`Changelog & License`
|
||||
title: $localize`Changelog`
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -1,23 +1,10 @@
|
||||
<div class="container">
|
||||
<div class="mb-5 row">
|
||||
<div class="col">
|
||||
<h3 class="mb-3 text-center" i18n>Changelog</h3>
|
||||
<mat-card appearance="outlined" class="changelog">
|
||||
<mat-card-content>
|
||||
<markdown [src]="'../assets/CHANGELOG.md'"></markdown>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3 class="mb-3 text-center" i18n>License</h3>
|
||||
<mat-card appearance="outlined">
|
||||
<mat-card-content>
|
||||
<markdown [src]="'../assets/LICENSE'"></markdown>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Changelog</h1>
|
||||
<div class="changelog">
|
||||
<markdown [src]="'../assets/CHANGELOG.md'"></markdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MarkdownModule } from 'ngx-markdown';
|
||||
|
||||
import { ChangelogPageRoutingModule } from './changelog-page-routing.module';
|
||||
@ -11,8 +10,7 @@ import { ChangelogPageComponent } from './changelog-page.component';
|
||||
imports: [
|
||||
ChangelogPageRoutingModule,
|
||||
CommonModule,
|
||||
MarkdownModule.forChild(),
|
||||
MatCardModule
|
||||
MarkdownModule.forChild()
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
|
@ -2,35 +2,33 @@
|
||||
color: rgb(var(--dark-primary-text));
|
||||
display: block;
|
||||
|
||||
.mat-mdc-card {
|
||||
&.changelog {
|
||||
::ng-deep {
|
||||
a {
|
||||
color: rgba(var(--palette-primary-500), 1);
|
||||
font-weight: 500;
|
||||
.changelog {
|
||||
::ng-deep {
|
||||
a {
|
||||
color: rgba(var(--palette-primary-500), 1);
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
color: rgba(var(--palette-primary-300), 1);
|
||||
&:hover {
|
||||
color: rgba(var(--palette-primary-300), 1);
|
||||
}
|
||||
}
|
||||
|
||||
markdown {
|
||||
h1,
|
||||
p {
|
||||
display: none;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 18px;
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
markdown {
|
||||
h1,
|
||||
p {
|
||||
display: none;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 18px;
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 15px;
|
||||
}
|
||||
h3 {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,20 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
|
||||
import { LicensePageComponent } from './license-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
component: LicensePageComponent,
|
||||
path: '',
|
||||
title: $localize`License`
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class LicensePageRoutingModule {}
|
@ -0,0 +1,19 @@
|
||||
import { Component, OnDestroy } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
selector: 'gf-license-page',
|
||||
styleUrls: ['./license-page.scss'],
|
||||
templateUrl: './license-page.html'
|
||||
})
|
||||
export class LicensePageComponent implements OnDestroy {
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor() {}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
}
|
10
apps/client/src/app/pages/about/license/license-page.html
Normal file
10
apps/client/src/app/pages/about/license/license-page.html
Normal file
@ -0,0 +1,10 @@
|
||||
<div class="container">
|
||||
<div class="mb-5 row">
|
||||
<div class="col">
|
||||
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>License</h1>
|
||||
<div>
|
||||
<markdown [src]="'../assets/LICENSE'"></markdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,13 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MarkdownModule } from 'ngx-markdown';
|
||||
|
||||
import { LicensePageRoutingModule } from './license-page-routing.module';
|
||||
import { LicensePageComponent } from './license-page.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [LicensePageComponent],
|
||||
imports: [LicensePageRoutingModule, CommonModule, MarkdownModule.forChild()],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class LicensePageModule {}
|
@ -0,0 +1,8 @@
|
||||
:host {
|
||||
color: rgb(var(--dark-primary-text));
|
||||
display: block;
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
color: rgb(var(--light-primary-text));
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
|
||||
import { AboutOverviewPageComponent } from './about-overview-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
component: AboutOverviewPageComponent,
|
||||
path: '',
|
||||
title: $localize`About`
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AboutOverviewPageRoutingModule {}
|
@ -0,0 +1,59 @@
|
||||
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 { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page' },
|
||||
selector: 'gf-about-overview-page',
|
||||
styleUrls: ['./about-overview-page.scss'],
|
||||
templateUrl: './about-overview-page.html'
|
||||
})
|
||||
export class AboutOverviewPageComponent implements OnDestroy, OnInit {
|
||||
public hasPermissionForBlog: boolean;
|
||||
public hasPermissionForSubscription: boolean;
|
||||
public isLoggedIn: boolean;
|
||||
public user: User;
|
||||
public version = environment.version;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor(
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
private userService: UserService
|
||||
) {
|
||||
const { globalPermissions } = this.dataService.fetchInfo();
|
||||
|
||||
this.hasPermissionForBlog = hasPermission(
|
||||
globalPermissions,
|
||||
permissions.enableBlog
|
||||
);
|
||||
|
||||
this.hasPermissionForSubscription = hasPermission(
|
||||
globalPermissions,
|
||||
permissions.enableSubscription
|
||||
);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
@ -0,0 +1,154 @@
|
||||
<div class="container">
|
||||
<div class="mb-5 row">
|
||||
<div class="col">
|
||||
<h3 class="d-none d-sm-block mb-3 text-center">About Ghostfolio</h3>
|
||||
<div class="about-container">
|
||||
<p>
|
||||
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
|
||||
<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 title="Open Startup" [routerLink]="['/open']">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
|
||||
>
|
||||
and is driven by the efforts of its
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio/graphs/contributors"
|
||||
title="Contributors to Ghostfolio"
|
||||
>contributors</a
|
||||
>.
|
||||
<ng-container *ngIf="version">
|
||||
This instance is running Ghostfolio {{ version }}.
|
||||
</ng-container>
|
||||
<ng-container *ngIf="hasPermissionForSubscription"
|
||||
>Check the system status at
|
||||
<a href="https://status.ghostfol.io" title="Ghostfolio Status"
|
||||
>status.ghostfol.io</a
|
||||
>.</ng-container
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
If you encounter a bug or would like to suggest an improvement or a
|
||||
new
|
||||
<a [routerLink]="['/features']">feature</a>, please join the
|
||||
Ghostfolio
|
||||
<a
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
title="Join the Ghostfolio Slack community"
|
||||
>Slack</a
|
||||
>
|
||||
community, tweet to
|
||||
<a
|
||||
href="https://twitter.com/ghostfolio_"
|
||||
title="Tweet to Ghostfolio on Twitter"
|
||||
>@ghostfolio_</a
|
||||
><ng-container *ngIf="user?.subscription?.type === 'Premium'"
|
||||
>, send an e-mail to
|
||||
<a href="mailto:hi@ghostfol.io" title="Send an e-mail"
|
||||
>hi@ghostfol.io</a
|
||||
></ng-container
|
||||
>
|
||||
or start a discussion at
|
||||
<a
|
||||
href="https://github.com/ghostfolio/ghostfolio"
|
||||
title="Find Ghostfolio on GitHub"
|
||||
>GitHub</a
|
||||
>.
|
||||
</p>
|
||||
<p class="text-center">
|
||||
<a
|
||||
class="mx-2"
|
||||
href="https://twitter.com/ghostfolio_"
|
||||
mat-icon-button
|
||||
title="Follow Ghostfolio on Twitter"
|
||||
>
|
||||
<ion-icon name="logo-twitter"></ion-icon>
|
||||
</a>
|
||||
<a
|
||||
*ngIf="user?.subscription?.type === 'Premium'"
|
||||
class="mx-2"
|
||||
href="mailto:hi@ghostfol.io"
|
||||
mat-icon-button
|
||||
title="Send an e-mail"
|
||||
>
|
||||
<ion-icon name="mail"></ion-icon>
|
||||
</a>
|
||||
<a
|
||||
class="mx-2"
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
mat-icon-button
|
||||
title="Join the Ghostfolio Slack community"
|
||||
>
|
||||
<ion-icon name="logo-slack"></ion-icon>
|
||||
</a>
|
||||
<a
|
||||
class="mx-2"
|
||||
href="https://github.com/ghostfolio/ghostfolio"
|
||||
mat-icon-button
|
||||
title="Find Ghostfolio on GitHub"
|
||||
>
|
||||
<ion-icon name="logo-github"></ion-icon>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
class="d-flex justify-content-center"
|
||||
>
|
||||
<div
|
||||
class="independent-and-bootstrapped-logo mb-2"
|
||||
title="Ghostfolio is an independent & bootstrapped business"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
*ngIf="!hasPermissionForSubscription"
|
||||
class="d-flex justify-content-center"
|
||||
>
|
||||
<a
|
||||
href="https://www.buymeacoffee.com/ghostfolio"
|
||||
target="_blank"
|
||||
title="Support Ghostfolio"
|
||||
><img
|
||||
class="mb-2"
|
||||
src="../assets/images/button-buy-me-a-coffee.png"
|
||||
width="180"
|
||||
/></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div *ngIf="hasPermissionForSubscription" class="col-md-6 col-xs-12 my-2">
|
||||
<a
|
||||
class="py-4 w-100"
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
[routerLink]="['/faq']"
|
||||
>Frequently Asked Questions (FAQ)</a
|
||||
>
|
||||
</div>
|
||||
<div *ngIf="hasPermissionForBlog" class="col-md-6 col-xs-12 my-2">
|
||||
<a
|
||||
class="py-4 w-100"
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
[routerLink]="['/blog']"
|
||||
>Blog</a
|
||||
>
|
||||
</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 { AboutOverviewPageRoutingModule } from './about-overview-page-routing.module';
|
||||
import { AboutOverviewPageComponent } from './about-overview-page.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AboutOverviewPageComponent],
|
||||
imports: [
|
||||
AboutOverviewPageRoutingModule,
|
||||
CommonModule,
|
||||
MatButtonModule,
|
||||
RouterModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class AboutOverviewPageModule {}
|
@ -0,0 +1,36 @@
|
||||
:host {
|
||||
color: rgb(var(--dark-primary-text));
|
||||
display: block;
|
||||
|
||||
.about-container {
|
||||
a {
|
||||
color: rgba(var(--palette-primary-500), 1);
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
color: rgba(var(--palette-primary-300), 1);
|
||||
}
|
||||
}
|
||||
|
||||
.independent-and-bootstrapped-logo {
|
||||
background-image: url('/assets/bootstrapped-dark.svg');
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
height: 2rem;
|
||||
opacity: 0.87;
|
||||
width: 10rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
color: rgb(var(--light-primary-text));
|
||||
|
||||
.about-container {
|
||||
.independent-and-bootstrapped-logo {
|
||||
background-image: url('/assets/bootstrapped-light.svg');
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
@ -156,10 +156,10 @@
|
||||
>Nederlands (<ng-container i18n>Community</ng-container
|
||||
>)</mat-option
|
||||
>
|
||||
<!--<mat-option value="pt"
|
||||
<mat-option value="pt"
|
||||
>Português (<ng-container i18n>Community</ng-container
|
||||
>)</mat-option
|
||||
>-->
|
||||
>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
@ -153,6 +153,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
||||
public openUpdateAccountDialog({
|
||||
accountType,
|
||||
balance,
|
||||
comment,
|
||||
currency,
|
||||
id,
|
||||
isExcluded,
|
||||
@ -164,6 +165,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
||||
account: {
|
||||
accountType,
|
||||
balance,
|
||||
comment,
|
||||
currency,
|
||||
id,
|
||||
isExcluded,
|
||||
@ -232,6 +234,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
||||
account: {
|
||||
accountType: AccountType.SECURITIES,
|
||||
balance: 0,
|
||||
comment: null,
|
||||
currency: this.user?.settings?.baseCurrency,
|
||||
isExcluded: false,
|
||||
name: null,
|
||||
|
@ -50,6 +50,19 @@
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Note</mat-label>
|
||||
<textarea
|
||||
cdkAutosizeMinRows="2"
|
||||
cdkTextareaAutosize
|
||||
matInput
|
||||
name="comment"
|
||||
[(ngModel)]="data.account.comment"
|
||||
(keyup.enter)="$event.stopPropagation()"
|
||||
></textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="mb-3 px-2">
|
||||
<mat-checkbox
|
||||
color="primary"
|
||||
|
@ -13,18 +13,17 @@ const routes: Routes = [
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
children: [
|
||||
{ path: '', redirectTo: 'overview', pathMatch: 'full' },
|
||||
{
|
||||
path: '',
|
||||
component: AdminOverviewComponent,
|
||||
title: $localize`Admin Control`
|
||||
},
|
||||
{ path: 'jobs', component: AdminJobsComponent, title: $localize`Jobs` },
|
||||
{
|
||||
path: 'market-data',
|
||||
component: AdminMarketDataComponent,
|
||||
title: $localize`Market Data`
|
||||
},
|
||||
{
|
||||
path: 'overview',
|
||||
component: AdminOverviewComponent,
|
||||
title: $localize`Admin Control`
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
component: AdminSettingsComponent,
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Component, HostBinding, OnDestroy, OnInit } from '@angular/core';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { TabConfiguration } from '@ghostfolio/common/interfaces';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
@ -13,7 +14,7 @@ export class AdminPageComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
public hasMessage: boolean;
|
||||
public tabs: { iconName: string; label: string; path: string }[] = [];
|
||||
public tabs: TabConfiguration[] = [];
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
@ -28,20 +29,28 @@ export class AdminPageComponent implements OnDestroy, OnInit {
|
||||
{
|
||||
iconName: 'reader-outline',
|
||||
label: $localize`Overview`,
|
||||
path: 'overview'
|
||||
path: ['/admin']
|
||||
},
|
||||
{
|
||||
iconName: 'settings-outline',
|
||||
label: $localize`Settings`,
|
||||
path: 'settings'
|
||||
path: ['/admin', 'settings']
|
||||
},
|
||||
{
|
||||
iconName: 'server-outline',
|
||||
label: $localize`Market Data`,
|
||||
path: 'market-data'
|
||||
path: ['/admin', 'market-data']
|
||||
},
|
||||
{ iconName: 'flash-outline', label: $localize`Jobs`, path: 'jobs' },
|
||||
{ iconName: 'people-outline', label: $localize`Users`, path: 'users' }
|
||||
{
|
||||
iconName: 'flash-outline',
|
||||
label: $localize`Jobs`,
|
||||
path: ['/admin', 'jobs']
|
||||
},
|
||||
{
|
||||
iconName: 'people-outline',
|
||||
label: $localize`Users`,
|
||||
path: ['/admin', 'users']
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -3,16 +3,19 @@
|
||||
</mat-tab-nav-panel>
|
||||
|
||||
<nav mat-align-tabs="center" mat-tab-nav-bar [tabPanel]="tabPanel">
|
||||
<a
|
||||
#rla="routerLinkActive"
|
||||
*ngFor="let tab of tabs"
|
||||
class="px-3"
|
||||
mat-tab-link
|
||||
routerLinkActive
|
||||
[active]="rla.isActive"
|
||||
[routerLink]="tab.path"
|
||||
>
|
||||
<ion-icon size="large" [name]="tab.iconName"></ion-icon>
|
||||
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
|
||||
</a>
|
||||
<ng-container *ngFor="let tab of tabs">
|
||||
<a
|
||||
#rla="routerLinkActive"
|
||||
*ngIf="tab.showCondition !== false"
|
||||
class="px-3"
|
||||
mat-tab-link
|
||||
routerLinkActive
|
||||
[active]="rla.isActive"
|
||||
[routerLink]="tab.path"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
>
|
||||
<ion-icon size="large" [name]="tab.iconName"></ion-icon>
|
||||
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
|
||||
</a>
|
||||
</ng-container>
|
||||
</nav>
|
||||
|
@ -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>
|
||||
|
@ -93,7 +93,10 @@
|
||||
</p>
|
||||
<p>
|
||||
I have already started to build a
|
||||
<a href="https://ghostfolio.slack.com">community</a>
|
||||
<a
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
>community</a
|
||||
>
|
||||
of users. In the future, I would like to involve more contributors
|
||||
to further extend the functionality of Ghostfolio (e.g. with new
|
||||
reports). Get in touch with me by e-mail at
|
||||
@ -179,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: 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>
|
||||
|
@ -29,7 +29,10 @@
|
||||
<p>
|
||||
The Ghostfolio community is growing on various platforms and has
|
||||
recently passed 100 members on
|
||||
<a href="https://ghostfolio.slack.com">Slack</a>
|
||||
<a
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
>Slack</a
|
||||
>
|
||||
as well as 100 followers on
|
||||
<a href="https://twitter.com/ghostfolio_">Twitter</a>. If you have
|
||||
not joined yet, this is a good time to make sure you do not miss out
|
||||
@ -191,7 +194,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>
|
||||
|
@ -80,8 +80,11 @@
|
||||
<h2 class="h4">Get support</h2>
|
||||
<p>
|
||||
If you have further questions or ideas, please join our growing
|
||||
<a href="https://ghostfolio.slack.com">Slack community</a> or get in
|
||||
touch on Twitter
|
||||
<a
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
>Slack</a
|
||||
>
|
||||
community or get in touch on Twitter
|
||||
<a href="https://twitter.com/ghostfolio_">@ghostfolio_</a> or by
|
||||
e-mail via <a href="mailto:hi@ghostfol.io">hi@ghostfol.io</a>.
|
||||
</p>
|
||||
@ -177,7 +180,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>
|
||||
|
@ -85,7 +85,9 @@
|
||||
<p>
|
||||
To participate in the ongoing development of Ghostfolio, please feel
|
||||
free to reach out to us on our
|
||||
<a href="https://ghostfolio.slack.com" target="_blank"
|
||||
<a
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
target="_blank"
|
||||
>Slack channel</a
|
||||
>
|
||||
or via Twitter
|
||||
@ -199,7 +201,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>
|
||||
|
@ -37,8 +37,11 @@
|
||||
empowers busy people to take control of their personal finances. In
|
||||
addition, Ghostfolio has attracted over 250 members from around the
|
||||
world to its
|
||||
<a href="https://ghostfolio.slack.com">Slack</a> community, where
|
||||
they can connect and share ideas about investing.
|
||||
<a
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
>Slack</a
|
||||
>
|
||||
community, where they can connect and share ideas about investing.
|
||||
</p>
|
||||
<p>
|
||||
<figure class="figure">
|
||||
@ -114,8 +117,12 @@
|
||||
and we look forward to collaborating and learning together. If you
|
||||
are a web developer and interested in personal finance, please join
|
||||
our
|
||||
<a href="https://ghostfolio.slack.com">Slack</a> channel or connect
|
||||
with <a href="https://twitter.com/ghostfolio_">@ghostfolio_</a> on
|
||||
<a
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
>Slack</a
|
||||
>
|
||||
community or connect with
|
||||
<a href="https://twitter.com/ghostfolio_">@ghostfolio_</a> on
|
||||
Twitter. We are happy to discuss ideas and get you involved.
|
||||
</p>
|
||||
<p>Thank you for all your feedback and support.</p>
|
||||
@ -244,7 +251,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>
|
||||
|
@ -15,8 +15,8 @@
|
||||
<section class="mb-4">
|
||||
<p>
|
||||
Managing personal finances effectively is crucial for those striving
|
||||
for financial independence and a secure future. In today’s digital
|
||||
age, having a reliable personal finance software can greatly
|
||||
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"
|
||||
@ -25,15 +25,15 @@
|
||||
>
|
||||
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 minimalism,
|
||||
pursuing a buy & hold strategy, seeking portfolio insights, and
|
||||
diversifying financial resources while valuing privacy.
|
||||
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 consolidated solution to efficiently monitor and
|
||||
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
|
||||
@ -61,7 +61,7 @@
|
||||
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 diversification.
|
||||
adjustments to optimize their allocation.
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-4">
|
||||
@ -79,7 +79,7 @@
|
||||
<section class="mb-4">
|
||||
<h2 class="h4">Streamlined Minimalism for Financial Efficiency</h2>
|
||||
<p>
|
||||
Ghostfolio embraces a minimalist approach to personal finance
|
||||
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
|
||||
@ -172,6 +172,9 @@
|
||||
<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>
|
||||
@ -227,7 +230,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"
|
||||
>
|
||||
Unlock your Financial Potential with Ghostfolio
|
||||
</li>
|
||||
</ol>
|
||||
|
@ -9,7 +9,7 @@ const routes: Routes = [
|
||||
canActivate: [AuthGuard],
|
||||
component: FaqPageComponent,
|
||||
path: '',
|
||||
title: $localize`FAQ`
|
||||
title: $localize`Frequently Asked Questions (FAQ)`
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -183,10 +183,11 @@
|
||||
feedback, bug reports, feature requests and of course contributions!
|
||||
You can reach us via Ghostfolio
|
||||
<a
|
||||
href="https://ghostfolio.slack.com"
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
title="Join the Ghostfolio Slack community"
|
||||
>Slack community</a
|
||||
>,
|
||||
>Slack</a
|
||||
>
|
||||
community,
|
||||
<a
|
||||
href="https://twitter.com/ghostfolio_"
|
||||
title="Tweet to Ghostfolio on Twitter"
|
||||
@ -212,10 +213,10 @@
|
||||
<mat-card-content
|
||||
>Please join the Ghostfolio
|
||||
<a
|
||||
href="https://ghostfolio.slack.com"
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
title="Join the Ghostfolio Slack community"
|
||||
>Slack community</a
|
||||
>, tweet to
|
||||
>Slack </a
|
||||
>community, tweet to
|
||||
<a
|
||||
href="https://twitter.com/ghostfolio_"
|
||||
title="Tweet to Ghostfolio on Twitter"
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3 class="d-none d-sm-block mb-3 text-center">Features</h3>
|
||||
<h3 class="d-none d-sm-block mb-3 text-center" i18n>Features</h3>
|
||||
<div class="mb-4">
|
||||
<p>
|
||||
Check out the numerous features of <strong>Ghostfolio</strong> to
|
||||
@ -13,7 +13,7 @@
|
||||
<mat-card appearance="outlined" class="d-flex flex-column h-100">
|
||||
<mat-card-content>
|
||||
<div class="flex-grow-1">
|
||||
<h4>Stocks</h4>
|
||||
<h4 i18n>Stocks</h4>
|
||||
<p class="m-0">Keep track of your stock purchases and sales.</p>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
@ -23,7 +23,7 @@
|
||||
<mat-card appearance="outlined" class="d-flex flex-column h-100">
|
||||
<mat-card-content>
|
||||
<div class="flex-grow-1">
|
||||
<h4>ETFs</h4>
|
||||
<h4 i18n>ETFs</h4>
|
||||
<p class="m-0">
|
||||
Are you into ETFs (Exchange Traded Funds)? Track your ETF
|
||||
investments.
|
||||
@ -36,7 +36,7 @@
|
||||
<mat-card appearance="outlined" class="d-flex flex-column h-100">
|
||||
<mat-card-content>
|
||||
<div class="flex-grow-1">
|
||||
<h4>Bonds</h4>
|
||||
<h4 i18n>Bonds</h4>
|
||||
<p class="m-0">
|
||||
Manage your investment in bonds and other assets with fixed
|
||||
income.
|
||||
@ -49,7 +49,7 @@
|
||||
<mat-card appearance="outlined" class="d-flex flex-column h-100">
|
||||
<mat-card-content>
|
||||
<div class="flex-grow-1">
|
||||
<h4>Cryptocurrencies</h4>
|
||||
<h4 i18n>Cryptocurrencies</h4>
|
||||
<p class="m-0">
|
||||
Keep track of your Bitcoin and Altcoin holdings.
|
||||
</p>
|
||||
@ -61,7 +61,7 @@
|
||||
<mat-card appearance="outlined" class="d-flex flex-column h-100">
|
||||
<mat-card-content>
|
||||
<div class="flex-grow-1">
|
||||
<h4>Dividend</h4>
|
||||
<h4 i18n>Dividend</h4>
|
||||
<p class="m-0">
|
||||
Are you building a dividend portfolio? Track your dividend in
|
||||
Ghostfolio.
|
||||
@ -74,7 +74,7 @@
|
||||
<mat-card appearance="outlined" class="d-flex flex-column h-100">
|
||||
<mat-card-content>
|
||||
<div class="flex-grow-1">
|
||||
<h4 class="align-items-center d-flex">Wealth Items</h4>
|
||||
<h4 class="align-items-center d-flex" i18n>Wealth Items</h4>
|
||||
<p class="m-0">
|
||||
Track all your treasuries, be it your luxury watch or rare
|
||||
trading cards.
|
||||
@ -87,7 +87,7 @@
|
||||
<mat-card appearance="outlined" class="d-flex flex-column h-100">
|
||||
<mat-card-content>
|
||||
<div class="flex-grow-1">
|
||||
<h4 class="align-items-center d-flex">Emergency Fund</h4>
|
||||
<h4 class="align-items-center d-flex" i18n>Emergency Fund</h4>
|
||||
<p class="m-0">
|
||||
Define your emergency fund you are comfortable with for
|
||||
difficult times.
|
||||
@ -100,7 +100,22 @@
|
||||
<mat-card appearance="outlined" class="d-flex flex-column h-100">
|
||||
<mat-card-content>
|
||||
<div class="flex-grow-1">
|
||||
<h4 class="align-items-center d-flex">Import and Export</h4>
|
||||
<h4 class="align-items-center d-flex" i18n>Liabilities</h4>
|
||||
<p class="m-0">
|
||||
Manage your financial liabilities, such as your student loan,
|
||||
to stay ahead of your financial obligations.
|
||||
</p>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-4 mb-3">
|
||||
<mat-card appearance="outlined" class="d-flex flex-column h-100">
|
||||
<mat-card-content>
|
||||
<div class="flex-grow-1">
|
||||
<h4 class="align-items-center d-flex" i18n>
|
||||
Import and Export
|
||||
</h4>
|
||||
<p class="m-0">Import and export your investment activities.</p>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
@ -110,7 +125,7 @@
|
||||
<mat-card appearance="outlined" class="d-flex flex-column h-100">
|
||||
<mat-card-content>
|
||||
<div class="flex-grow-1">
|
||||
<h4>Multi-Accounts</h4>
|
||||
<h4 i18n>Multi-Accounts</h4>
|
||||
<p class="m-0">
|
||||
Keep an eye on all your accounts across multiple platforms
|
||||
(multi-banking).
|
||||
@ -124,7 +139,7 @@
|
||||
<mat-card-content>
|
||||
<div class="flex-grow-1">
|
||||
<h4 class="align-items-center d-flex">
|
||||
<span>Portfolio Calculations</span>
|
||||
<span i18n>Portfolio Calculations</span>
|
||||
<gf-premium-indicator
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
class="ml-1"
|
||||
@ -144,7 +159,7 @@
|
||||
<mat-card-content>
|
||||
<div class="flex-grow-1">
|
||||
<h4 class="align-items-center d-flex">
|
||||
<span>Portfolio Allocations</span>
|
||||
<span i18n>Portfolio Allocations</span>
|
||||
<gf-premium-indicator
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
class="ml-1"
|
||||
@ -162,7 +177,7 @@
|
||||
<mat-card appearance="outlined" class="d-flex flex-column h-100">
|
||||
<mat-card-content>
|
||||
<div class="flex-grow-1">
|
||||
<h4 class="align-items-center d-flex">Dark Mode</h4>
|
||||
<h4 class="align-items-center d-flex" i18n>Dark Mode</h4>
|
||||
<p class="m-0">
|
||||
Ghostfolio automatically switches to a dark color theme based
|
||||
on your operating system's preferences.
|
||||
@ -175,7 +190,7 @@
|
||||
<mat-card appearance="outlined" class="d-flex flex-column h-100">
|
||||
<mat-card-content>
|
||||
<div class="flex-grow-1">
|
||||
<h4 class="align-items-center d-flex">Zen Mode</h4>
|
||||
<h4 class="align-items-center d-flex" i18n>Zen Mode</h4>
|
||||
<p class="m-0">
|
||||
Keep calm and activate Zen Mode if the markets are going
|
||||
crazy.
|
||||
@ -192,7 +207,7 @@
|
||||
<mat-card-content>
|
||||
<div class="flex-grow-1">
|
||||
<h4 class="align-items-center d-flex">
|
||||
<span>Market Mood</span>
|
||||
<span i18n>Market Mood</span>
|
||||
<gf-premium-indicator class="ml-1"></gf-premium-indicator>
|
||||
</h4>
|
||||
<p class="m-0">
|
||||
@ -210,7 +225,7 @@
|
||||
<mat-card-content>
|
||||
<div class="flex-grow-1">
|
||||
<h4 class="align-items-center d-flex">
|
||||
<span>Static Analysis</span>
|
||||
<span i18n>Static Analysis</span>
|
||||
<gf-premium-indicator
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
class="ml-1"
|
||||
@ -228,13 +243,11 @@
|
||||
<mat-card appearance="outlined" class="d-flex flex-column h-100">
|
||||
<mat-card-content>
|
||||
<div class="flex-grow-1">
|
||||
<h4>Multi-Language</h4>
|
||||
<h4 i18n>Multi-Language</h4>
|
||||
<p class="m-0">
|
||||
Use Ghostfolio in multiple languages: English, Dutch, French,
|
||||
German, Italian<ng-container *ngIf="false"
|
||||
>, Portuguese</ng-container
|
||||
>
|
||||
and Spanish are currently supported.
|
||||
German, Italian, Portuguese and Spanish are currently
|
||||
supported.
|
||||
</p>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
@ -244,16 +257,16 @@
|
||||
<mat-card appearance="outlined" class="d-flex flex-column h-100">
|
||||
<mat-card-content>
|
||||
<div class="flex-grow-1">
|
||||
<h4>Community</h4>
|
||||
<h4 i18n>Community</h4>
|
||||
<p class="m-0">
|
||||
Join the Ghostfolio
|
||||
<a
|
||||
href="https://ghostfolio.slack.com"
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
title="Join the Ghostfolio Slack community"
|
||||
>Slack channel</a
|
||||
>Slack</a
|
||||
>
|
||||
full of enthusiastic investors and discuss the latest market
|
||||
trends.
|
||||
community full of enthusiastic investors and discuss the
|
||||
latest market trends.
|
||||
</p>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
@ -263,7 +276,7 @@
|
||||
<mat-card appearance="outlined" class="d-flex flex-column h-100">
|
||||
<mat-card-content>
|
||||
<div class="flex-grow-1">
|
||||
<h4>Open Source Software</h4>
|
||||
<h4 i18n>Open Source Software</h4>
|
||||
<p class="m-0">
|
||||
The source code is fully available as
|
||||
<a
|
||||
@ -282,9 +295,9 @@
|
||||
</div>
|
||||
<div *ngIf="!user" class="row">
|
||||
<div class="col mt-3 text-center">
|
||||
<a color="primary" mat-flat-button [routerLink]="['/register']">
|
||||
Get Started
|
||||
</a>
|
||||
<a color="primary" i18n mat-flat-button [routerLink]="['/register']"
|
||||
>Get Started</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -12,9 +12,8 @@ const routes: Routes = [
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
children: [
|
||||
{ path: '', redirectTo: 'overview', pathMatch: 'full' },
|
||||
{
|
||||
path: 'overview',
|
||||
path: '',
|
||||
component: HomeOverviewComponent
|
||||
},
|
||||
{
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
} from '@angular/core';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { InfoItem, User } from '@ghostfolio/common/interfaces';
|
||||
import { TabConfiguration, User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
@ -24,8 +24,7 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
||||
|
||||
public hasMessage: boolean;
|
||||
public hasPermissionToAccessFearAndGreedIndex: boolean;
|
||||
public info: InfoItem;
|
||||
public tabs: { iconName: string; label: string; path: string }[] = [];
|
||||
public tabs: TabConfiguration[] = [];
|
||||
public user: User;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
@ -35,7 +34,12 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
||||
private dataService: DataService,
|
||||
private userService: UserService
|
||||
) {
|
||||
this.info = this.dataService.fetchInfo();
|
||||
const { globalPermissions, systemMessage } = this.dataService.fetchInfo();
|
||||
|
||||
this.hasPermissionToAccessFearAndGreedIndex = hasPermission(
|
||||
globalPermissions,
|
||||
permissions.enableFearAndGreedIndex
|
||||
);
|
||||
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
@ -45,17 +49,23 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
||||
{
|
||||
iconName: 'analytics-outline',
|
||||
label: $localize`Overview`,
|
||||
path: 'overview'
|
||||
path: ['/home']
|
||||
},
|
||||
{
|
||||
iconName: 'wallet-outline',
|
||||
label: $localize`Holdings`,
|
||||
path: 'holdings'
|
||||
path: ['/home', 'holdings']
|
||||
},
|
||||
{
|
||||
iconName: 'reader-outline',
|
||||
label: $localize`Summary`,
|
||||
path: 'summary'
|
||||
path: ['/home', 'summary']
|
||||
},
|
||||
{
|
||||
iconName: 'newspaper-outline',
|
||||
label: $localize`Markets`,
|
||||
path: ['/home', 'market'],
|
||||
showCondition: this.hasPermissionToAccessFearAndGreedIndex
|
||||
}
|
||||
];
|
||||
this.user = state.user;
|
||||
@ -64,20 +74,7 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
||||
hasPermission(
|
||||
this.user?.permissions,
|
||||
permissions.createUserAccount
|
||||
) || !!this.info.systemMessage;
|
||||
|
||||
this.hasPermissionToAccessFearAndGreedIndex = hasPermission(
|
||||
this.info?.globalPermissions,
|
||||
permissions.enableFearAndGreedIndex
|
||||
);
|
||||
|
||||
if (this.hasPermissionToAccessFearAndGreedIndex) {
|
||||
this.tabs.push({
|
||||
iconName: 'newspaper-outline',
|
||||
label: $localize`Markets`,
|
||||
path: 'market'
|
||||
});
|
||||
}
|
||||
) || !!systemMessage;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
@ -3,15 +3,19 @@
|
||||
</mat-tab-nav-panel>
|
||||
|
||||
<nav mat-align-tabs="center" mat-tab-nav-bar [tabPanel]="tabPanel">
|
||||
<a
|
||||
#rla="routerLinkActive"
|
||||
*ngFor="let tab of tabs"
|
||||
mat-tab-link
|
||||
routerLinkActive
|
||||
[active]="rla.isActive"
|
||||
[routerLink]="tab.path"
|
||||
>
|
||||
<ion-icon size="large" [name]="tab.iconName"></ion-icon>
|
||||
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
|
||||
</a>
|
||||
<ng-container *ngFor="let tab of tabs">
|
||||
<a
|
||||
#rla="routerLinkActive"
|
||||
*ngIf="tab.showCondition !== false"
|
||||
class="px-3"
|
||||
mat-tab-link
|
||||
routerLinkActive
|
||||
[active]="rla.isActive"
|
||||
[routerLink]="tab.path"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
>
|
||||
<ion-icon size="large" [name]="tab.iconName"></ion-icon>
|
||||
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
|
||||
</a>
|
||||
</ng-container>
|
||||
</nav>
|
||||
|
@ -59,6 +59,19 @@ export class LandingPageComponent implements OnDestroy, OnInit {
|
||||
country: 'Germany 🇩🇪',
|
||||
quote:
|
||||
'Super slim app with a great user interface. On top of that, it’s open source.'
|
||||
},
|
||||
{
|
||||
author: 'Sal',
|
||||
country: 'Canada 🇨🇦',
|
||||
quote:
|
||||
'Ghostfolio is one of the best tools I have used for tracking my investments. I intend to spread the word to all my friends.'
|
||||
},
|
||||
{
|
||||
author: 'Thomas',
|
||||
country: 'Creator of Ghostfolio, Switzerland 🇨🇭',
|
||||
quote:
|
||||
'My investment strategy has become more structured through the daily use of Ghostfolio.',
|
||||
url: 'https://dotsilver.ch'
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -57,7 +57,10 @@
|
||||
>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-4 my-2">
|
||||
<a class="d-block" href="https://ghostfolio.slack.com">
|
||||
<a
|
||||
class="d-block"
|
||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||
>
|
||||
<gf-value
|
||||
size="large"
|
||||
[locale]="user?.settings?.locale"
|
||||
|
@ -291,7 +291,6 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
||||
date: new Date(),
|
||||
id: null,
|
||||
fee: 0,
|
||||
quantity: null,
|
||||
type: aActivity?.type ?? 'BUY',
|
||||
unitPrice: null
|
||||
},
|
||||
|
@ -55,8 +55,6 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
||||
public currencies: string[] = [];
|
||||
public currentMarketPrice = null;
|
||||
public defaultDateFormat: string;
|
||||
public filteredLookupItems: LookupItem[] = [];
|
||||
public filteredLookupItemsObservable: Observable<LookupItem[]> = of([]);
|
||||
public filteredTagsObservable: Observable<Tag[]> = of([]);
|
||||
public isLoading = false;
|
||||
public platforms: { id: string; name: string }[];
|
||||
@ -120,10 +118,12 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
||||
name: [this.data.activity?.SymbolProfile?.name, Validators.required],
|
||||
quantity: [this.data.activity?.quantity, Validators.required],
|
||||
searchSymbol: [
|
||||
{
|
||||
dataSource: this.data.activity?.SymbolProfile?.dataSource,
|
||||
symbol: this.data.activity?.SymbolProfile?.symbol
|
||||
},
|
||||
!!this.data.activity?.SymbolProfile
|
||||
? {
|
||||
dataSource: this.data.activity?.SymbolProfile?.dataSource,
|
||||
symbol: this.data.activity?.SymbolProfile?.symbol
|
||||
}
|
||||
: null,
|
||||
Validators.required
|
||||
],
|
||||
tags: [
|
||||
@ -238,28 +238,19 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.filteredLookupItemsObservable = this.activityForm.controls[
|
||||
'searchSymbol'
|
||||
].valueChanges.pipe(
|
||||
debounceTime(400),
|
||||
distinctUntilChanged(),
|
||||
switchMap((query: string) => {
|
||||
if (isString(query) && query.length > 1) {
|
||||
const filteredLookupItemsObservable =
|
||||
this.dataService.fetchSymbols(query);
|
||||
this.activityForm.controls['searchSymbol'].valueChanges.subscribe(() => {
|
||||
if (this.activityForm.controls['searchSymbol'].invalid) {
|
||||
this.data.activity.SymbolProfile = null;
|
||||
} else {
|
||||
this.activityForm.controls['dataSource'].setValue(
|
||||
this.activityForm.controls['searchSymbol'].value.dataSource
|
||||
);
|
||||
|
||||
filteredLookupItemsObservable
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((filteredLookupItems) => {
|
||||
this.filteredLookupItems = filteredLookupItems;
|
||||
});
|
||||
this.updateSymbol();
|
||||
}
|
||||
|
||||
return filteredLookupItemsObservable;
|
||||
}
|
||||
|
||||
return [];
|
||||
})
|
||||
);
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.filteredTagsObservable = this.activityForm.controls[
|
||||
'tags'
|
||||
@ -300,6 +291,33 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
||||
this.activityForm.controls['searchSymbol'].updateValueAndValidity();
|
||||
this.activityForm.controls['updateAccountBalance'].disable();
|
||||
this.activityForm.controls['updateAccountBalance'].setValue(false);
|
||||
} else if (type === 'LIABILITY') {
|
||||
this.activityForm.controls['accountId'].removeValidators(
|
||||
Validators.required
|
||||
);
|
||||
this.activityForm.controls['accountId'].updateValueAndValidity();
|
||||
this.activityForm.controls['currency'].setValue(
|
||||
this.data.user.settings.baseCurrency
|
||||
);
|
||||
this.activityForm.controls['currencyOfFee'].setValue(
|
||||
this.data.user.settings.baseCurrency
|
||||
);
|
||||
this.activityForm.controls['currencyOfUnitPrice'].setValue(
|
||||
this.data.user.settings.baseCurrency
|
||||
);
|
||||
this.activityForm.controls['dataSource'].removeValidators(
|
||||
Validators.required
|
||||
);
|
||||
this.activityForm.controls['dataSource'].updateValueAndValidity();
|
||||
this.activityForm.controls['name'].setValidators(Validators.required);
|
||||
this.activityForm.controls['name'].updateValueAndValidity();
|
||||
this.activityForm.controls['quantity'].setValue(1);
|
||||
this.activityForm.controls['searchSymbol'].removeValidators(
|
||||
Validators.required
|
||||
);
|
||||
this.activityForm.controls['searchSymbol'].updateValueAndValidity();
|
||||
this.activityForm.controls['updateAccountBalance'].disable();
|
||||
this.activityForm.controls['updateAccountBalance'].setValue(false);
|
||||
} else {
|
||||
this.activityForm.controls['accountId'].setValidators(
|
||||
Validators.required
|
||||
@ -366,25 +384,6 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
||||
this.tagInput.nativeElement.value = '';
|
||||
}
|
||||
|
||||
public onBlurSymbol() {
|
||||
const currentLookupItem = this.filteredLookupItems.find((lookupItem) => {
|
||||
return (
|
||||
lookupItem.symbol ===
|
||||
this.activityForm.controls['searchSymbol'].value.symbol
|
||||
);
|
||||
});
|
||||
|
||||
if (currentLookupItem) {
|
||||
this.updateSymbol(currentLookupItem.symbol);
|
||||
} else {
|
||||
this.activityForm.controls['searchSymbol'].setErrors({ incorrect: true });
|
||||
|
||||
this.data.activity.SymbolProfile = null;
|
||||
}
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
public onCancel() {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
@ -428,13 +427,6 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
||||
this.dialogRef.close({ activity });
|
||||
}
|
||||
|
||||
public onUpdateSymbol(event: MatAutocompleteSelectedEvent) {
|
||||
this.activityForm.controls['dataSource'].setValue(
|
||||
event.option.value.dataSource
|
||||
);
|
||||
this.updateSymbol(event.option.value.symbol);
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
@ -450,12 +442,8 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
private updateSymbol(symbol: string) {
|
||||
private updateSymbol() {
|
||||
this.isLoading = true;
|
||||
|
||||
this.activityForm.controls['searchSymbol'].setErrors(null);
|
||||
this.activityForm.controls['searchSymbol'].setValue({ symbol });
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
|
||||
this.dataService
|
||||
|
@ -14,6 +14,7 @@
|
||||
<mat-option i18n value="BUY">Buy</mat-option>
|
||||
<mat-option i18n value="DIVIDEND">Dividend</mat-option>
|
||||
<mat-option i18n value="ITEM">Item</mat-option>
|
||||
<mat-option i18n value="LIABILITY">Liability</mat-option>
|
||||
<mat-option i18n value="SELL">Sell</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
@ -47,36 +48,10 @@
|
||||
>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Name, symbol or ISIN</mat-label>
|
||||
<input
|
||||
autocapitalize="off"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
<gf-symbol-autocomplete
|
||||
formControlName="searchSymbol"
|
||||
matInput
|
||||
[matAutocomplete]="symbolAutocomplete"
|
||||
(blur)="onBlurSymbol()"
|
||||
[isLoading]="isLoading"
|
||||
/>
|
||||
<mat-autocomplete
|
||||
#symbolAutocomplete="matAutocomplete"
|
||||
[displayWith]="displayFn"
|
||||
(optionSelected)="onUpdateSymbol($event)"
|
||||
>
|
||||
<ng-container>
|
||||
<mat-option
|
||||
*ngFor="let lookupItem of filteredLookupItemsObservable | async"
|
||||
class="line-height-1"
|
||||
[value]="lookupItem"
|
||||
>
|
||||
<span><b>{{ lookupItem.name }}</b></span>
|
||||
<br />
|
||||
<small class="text-muted"
|
||||
>{{ lookupItem.symbol | gfSymbol }} · {{ lookupItem.currency
|
||||
}}</small
|
||||
>
|
||||
</mat-option>
|
||||
</ng-container>
|
||||
</mat-autocomplete>
|
||||
<mat-spinner *ngIf="isLoading" matSuffix [diameter]="20"></mat-spinner>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div
|
||||
@ -118,7 +93,10 @@
|
||||
<mat-datepicker #date disabled="false"></mat-datepicker>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div
|
||||
class="mb-3"
|
||||
[ngClass]="{ 'd-none': activityForm.controls['type']?.value === 'ITEM' || activityForm.controls['type']?.value === 'LIABILITY' }"
|
||||
>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Quantity</mat-label>
|
||||
<input formControlName="quantity" matInput type="number" />
|
||||
@ -132,6 +110,7 @@
|
||||
>Dividend</ng-container
|
||||
>
|
||||
<ng-container *ngSwitchCase="'ITEM'" i18n>Value</ng-container>
|
||||
<ng-container *ngSwitchCase="'LIABILITY'" i18n>Value</ng-container>
|
||||
<ng-container *ngSwitchDefault i18n>Unit Price</ng-container>
|
||||
</ng-container>
|
||||
</mat-label>
|
||||
@ -179,6 +158,7 @@
|
||||
>Dividend</ng-container
|
||||
>
|
||||
<ng-container *ngSwitchCase="'ITEM'" i18n>Value</ng-container>
|
||||
<ng-container *ngSwitchCase="'LIABILITY'" i18n>Value</ng-container>
|
||||
<ng-container *ngSwitchDefault i18n>Unit Price</ng-container>
|
||||
</ng-container>
|
||||
</mat-label>
|
||||
@ -188,7 +168,10 @@
|
||||
>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div
|
||||
class="mb-3"
|
||||
[ngClass]="{ 'd-none': activityForm.controls['type']?.value === 'ITEM' || activityForm.controls['type']?.value === 'LIABILITY' }"
|
||||
>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Fee</mat-label>
|
||||
<input formControlName="feeInCustomCurrency" matInput type="number" />
|
||||
@ -304,8 +287,9 @@
|
||||
<div class="d-flex" mat-dialog-actions>
|
||||
<gf-value
|
||||
class="flex-grow-1"
|
||||
[currency]="activityForm.controls['currency']?.value ?? data.user?.settings?.baseCurrency"
|
||||
[isCurrency]="true"
|
||||
[locale]="data.user?.settings?.locale"
|
||||
[unit]="activityForm.controls['currency']?.value ?? data.user?.settings?.baseCurrency"
|
||||
[value]="total"
|
||||
></gf-value>
|
||||
<div>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user