diff --git a/CHANGELOG.md b/CHANGELOG.md
index a090e6c555..90e2175e59 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,8 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
+- Added the performance timeline grouped by year to the analysis page (experimental)
- Extended the `GET api/v2/portfolio/performance` endpoint by the `groupBy` query parameter (`year`)
+### Changed
+
+- Harmonized the colors in the investment chart component
+- Improved the language localization for Chinese (`zh`)
+
### Fixed
- Fixed an issue where a user could create a custom asset profile with a symbol of the prefix `GF_`
diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts
index 109b5cca59..72468f1d6e 100644
--- a/apps/api/src/app/portfolio/portfolio.controller.ts
+++ b/apps/api/src/app/portfolio/portfolio.controller.ts
@@ -4,7 +4,8 @@ import { Impersonation } from '@ghostfolio/api/decorators/impersonation.decorato
import { RequiresScope } from '@ghostfolio/api/decorators/requires-scope.decorator';
import {
hasNotDefinedValuesInObject,
- nullifyValuesInObject
+ nullifyValuesInObject,
+ nullifyValuesInObjects
} from '@ghostfolio/api/helper/object.helper';
import { convertValuesToPercentages } from '@ghostfolio/api/helper/portfolio.helper';
import { PerformanceLoggingInterceptor } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor';
@@ -583,14 +584,22 @@ export class PortfolioController {
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription?.type === SubscriptionType.Basic
) {
- performanceInformation.chart = performanceInformation.chart.map(
- (item) => {
- return nullifyValuesInObject(item, [
- 'dividendInBaseCurrency',
- 'totalInvestment',
- 'value'
- ]);
- }
+ performanceInformation.chart = nullifyValuesInObjects(
+ performanceInformation.chart,
+ [
+ 'dividendInBaseCurrency',
+ 'totalInvestment',
+ 'value',
+ ...(groupBy === 'year'
+ ? [
+ 'investmentValueWithCurrencyEffect',
+ 'netPerformance',
+ 'netPerformanceInPercentage',
+ 'netPerformanceInPercentageWithCurrencyEffect',
+ 'netPerformanceWithCurrencyEffect'
+ ]
+ : [])
+ ]
);
performanceInformation.performance = nullifyValuesInObject(
performanceInformation.performance,
diff --git a/apps/client/src/app/app.component.html b/apps/client/src/app/app.component.html
index 61d5023e2d..5e2b658a28 100644
--- a/apps/client/src/app/app.component.html
+++ b/apps/client/src/app/app.component.html
@@ -48,6 +48,6 @@
@if (showFooter) {
}
diff --git a/apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html b/apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html
index f923b95a6e..4109fd3cb4 100644
--- a/apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html
+++ b/apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html
@@ -28,12 +28,8 @@
matInput
(keydown.enter)="$event.stopPropagation()"
/>
- @if (platformForm.get('url')?.value) {
-
+ @if (platformForm.get('url')?.value; as url) {
+
}
diff --git a/apps/client/src/app/components/footer/footer.component.html b/apps/client/src/app/components/footer/footer.component.html
index 5f0016317e..6dc44f35ee 100644
--- a/apps/client/src/app/components/footer/footer.component.html
+++ b/apps/client/src/app/components/footer/footer.component.html
@@ -6,7 +6,7 @@
Personal Finance
- @if (hasPermissionToAccessFearAndGreedIndex) {
+ @if (hasPermissionToAccessFearAndGreedIndex()) {
-
Markets
@@ -18,7 +18,7 @@
Ghostfolio
- About
- @if (hasPermissionForSubscription) {
+ @if (hasPermissionForSubscription()) {
-
Blog
@@ -27,43 +27,43 @@
Changelog
- Features
- @if (hasPermissionForSubscription) {
+ @if (hasPermissionForSubscription()) {
-
Frequently Asked Questions (FAQ)
}
- @if (!hasPermissionForSubscription) {
+ @if (!hasPermissionForSubscription()) {
-
License
}
- @if (hasPermissionForStatistics) {
+ @if (hasPermissionForStatistics()) {
-
Open Startup
}
- @if (hasPermissionForSubscription) {
+ @if (hasPermissionForSubscription()) {
-
Pricing
}
- @if (hasPermissionForSubscription) {
+ @if (hasPermissionForSubscription()) {
-
Privacy Policy
}
- @if (hasPermissionForSubscription) {
+ @if (hasPermissionForSubscription()) {
-
Terms of Service
}
- @if (hasPermissionForSubscription) {
+ @if (hasPermissionForSubscription()) {
-
();
- public currentYear = new Date().getFullYear();
- public hasPermissionForStatistics: boolean;
- public hasPermissionForSubscription: boolean;
- public hasPermissionToAccessFearAndGreedIndex: boolean;
- public routerLinkAbout = publicRoutes.about.routerLink;
- public routerLinkAboutChangelog =
- publicRoutes.about.subRoutes.changelog.routerLink;
- public routerLinkAboutLicense =
- publicRoutes.about.subRoutes.license.routerLink;
- public routerLinkAboutPrivacyPolicy =
- publicRoutes.about.subRoutes.privacyPolicy.routerLink;
- public routerLinkAboutTermsOfService =
- publicRoutes.about.subRoutes.termsOfService.routerLink;
- public routerLinkBlog = publicRoutes.blog.routerLink;
- public routerLinkFaq = publicRoutes.faq.routerLink;
- public routerLinkFeatures = publicRoutes.features.routerLink;
- public routerLinkMarkets = publicRoutes.markets.routerLink;
- public routerLinkOpenStartup = publicRoutes.openStartup.routerLink;
- public routerLinkPricing = publicRoutes.pricing.routerLink;
- public routerLinkResources = publicRoutes.resources.routerLink;
-
- public constructor() {
- addIcons({
- openOutline
- });
- }
+ protected readonly currentYear = new Date().getFullYear();
- public ngOnChanges() {
- this.hasPermissionForStatistics = hasPermission(
- this.info?.globalPermissions,
+ protected readonly hasPermissionForStatistics = computed(() => {
+ return hasPermission(
+ this.info()?.globalPermissions,
permissions.enableStatistics
);
+ });
- this.hasPermissionForSubscription = hasPermission(
- this.info?.globalPermissions,
+ protected readonly hasPermissionForSubscription = computed(() => {
+ return hasPermission(
+ this.info()?.globalPermissions,
permissions.enableSubscription
);
+ });
- this.hasPermissionToAccessFearAndGreedIndex = hasPermission(
- this.info?.globalPermissions,
+ protected readonly hasPermissionToAccessFearAndGreedIndex = computed(() => {
+ return hasPermission(
+ this.info()?.globalPermissions,
permissions.enableFearAndGreedIndex
);
+ });
+
+ protected readonly routerLinkAbout = publicRoutes.about.routerLink;
+ protected readonly routerLinkAboutChangelog =
+ publicRoutes.about.subRoutes.changelog.routerLink;
+ protected readonly routerLinkAboutLicense =
+ publicRoutes.about.subRoutes.license.routerLink;
+ protected readonly routerLinkAboutPrivacyPolicy =
+ publicRoutes.about.subRoutes.privacyPolicy.routerLink;
+ protected readonly routerLinkAboutTermsOfService =
+ publicRoutes.about.subRoutes.termsOfService.routerLink;
+ protected readonly routerLinkBlog = publicRoutes.blog.routerLink;
+ protected readonly routerLinkFaq = publicRoutes.faq.routerLink;
+ protected readonly routerLinkFeatures = publicRoutes.features.routerLink;
+ protected readonly routerLinkMarkets = publicRoutes.markets.routerLink;
+ protected readonly routerLinkOpenStartup =
+ publicRoutes.openStartup.routerLink;
+ protected readonly routerLinkPricing = publicRoutes.pricing.routerLink;
+ protected readonly routerLinkResources = publicRoutes.resources.routerLink;
+
+ public constructor() {
+ addIcons({
+ openOutline
+ });
}
}
diff --git a/apps/client/src/app/components/header/header.component.html b/apps/client/src/app/components/header/header.component.html
index eaccbb3326..48ed63ab34 100644
--- a/apps/client/src/app/components/header/header.component.html
+++ b/apps/client/src/app/components/header/header.component.html
@@ -124,7 +124,7 @@
>About
- @if (hasPermissionToAccessAssistant) {
+ @if (hasPermissionToAccessAssistant && user(); as user) {
-