Browse Source

Merge branch 'main' into bugfix/creation-of-custom-asset-profiles-with-ghostfolio-prefix

pull/8070/head
Thomas Kaul 5 days ago
committed by GitHub
parent
commit
fa93e02035
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      CHANGELOG.md
  2. 27
      apps/api/src/app/portfolio/portfolio.controller.ts
  3. 2
      apps/client/src/app/app.component.html
  4. 8
      apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html
  5. 18
      apps/client/src/app/components/footer/footer.component.html
  6. 80
      apps/client/src/app/components/footer/footer.component.ts
  7. 10
      apps/client/src/app/components/header/header.component.html
  8. 56
      apps/client/src/app/components/home-holdings/home-holdings.component.ts
  9. 10
      apps/client/src/app/components/home-holdings/home-holdings.html
  10. 8
      apps/client/src/app/components/home-overview/home-overview.component.ts
  11. 13
      apps/client/src/app/components/home-overview/home-overview.html
  12. 23
      apps/client/src/app/components/investment-chart/investment-chart.component.ts
  13. 14
      apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts
  14. 4
      apps/client/src/app/components/user-account-membership/user-account-membership.html
  15. 8
      apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html
  16. 44
      apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
  17. 29
      apps/client/src/app/pages/portfolio/analysis/analysis-page.html
  18. 7
      apps/client/src/app/util/date.util.ts
  19. 54
      apps/client/src/locales/messages.zh.xlf
  20. 2
      libs/ui/src/lib/line-chart/line-chart.component.ts
  21. 7
      libs/ui/src/lib/services/data.service.ts

6
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_`

27
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,

2
apps/client/src/app/app.component.html

@ -48,6 +48,6 @@
@if (showFooter) {
<footer class="justify-content-center overflow-hidden w-100">
<gf-footer class="py-4" [info]="info" [user]="user" />
<gf-footer class="py-4" [info]="info" />
</footer>
}

8
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) {
<gf-entity-logo
class="mat-icon"
matSuffix
[url]="platformForm.get('url').value"
/>
@if (platformForm.get('url')?.value; as url) {
<gf-entity-logo class="mat-icon" matSuffix [url]="url" />
}
</mat-form-field>
</div>

18
apps/client/src/app/components/footer/footer.component.html

@ -6,7 +6,7 @@
<div class="col-sm">
<div class="h6 mt-2" i18n>Personal Finance</div>
<ul class="list-unstyled">
@if (hasPermissionToAccessFearAndGreedIndex) {
@if (hasPermissionToAccessFearAndGreedIndex()) {
<li>
<a i18n [routerLink]="routerLinkMarkets">Markets</a>
</li>
@ -18,7 +18,7 @@
<div class="h6 mt-2">Ghostfolio</div>
<ul class="list-unstyled">
<li><a i18n [routerLink]="routerLinkAbout">About</a></li>
@if (hasPermissionForSubscription) {
@if (hasPermissionForSubscription()) {
<li>
<a i18n [routerLink]="routerLinkBlog">Blog</a>
</li>
@ -27,43 +27,43 @@
<a i18n [routerLink]="routerLinkAboutChangelog">Changelog</a>
</li>
<li><a i18n [routerLink]="routerLinkFeatures">Features</a></li>
@if (hasPermissionForSubscription) {
@if (hasPermissionForSubscription()) {
<li>
<a i18n [routerLink]="routerLinkFaq"
>Frequently Asked Questions (FAQ)</a
>
</li>
}
@if (!hasPermissionForSubscription) {
@if (!hasPermissionForSubscription()) {
<li>
<a i18n [routerLink]="routerLinkAboutLicense">License</a>
</li>
}
@if (hasPermissionForStatistics) {
@if (hasPermissionForStatistics()) {
<li>
<a [routerLink]="routerLinkOpenStartup">Open Startup</a>
</li>
}
@if (hasPermissionForSubscription) {
@if (hasPermissionForSubscription()) {
<li>
<a i18n [routerLink]="routerLinkPricing">Pricing</a>
</li>
}
@if (hasPermissionForSubscription) {
@if (hasPermissionForSubscription()) {
<li>
<a i18n [routerLink]="routerLinkAboutPrivacyPolicy"
>Privacy Policy</a
>
</li>
}
@if (hasPermissionForSubscription) {
@if (hasPermissionForSubscription()) {
<li>
<a i18n [routerLink]="routerLinkAboutTermsOfService"
>Terms of Service</a
>
</li>
}
@if (hasPermissionForSubscription) {
@if (hasPermissionForSubscription()) {
<li>
<a
class="align-items-baseline d-flex"

80
apps/client/src/app/components/footer/footer.component.ts

@ -1,4 +1,4 @@
import { InfoItem, User } from '@ghostfolio/common/interfaces';
import { InfoItem } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { publicRoutes } from '@ghostfolio/common/routes/routes';
import { GfLogoComponent } from '@ghostfolio/ui/logo';
@ -6,9 +6,9 @@ import { GfLogoComponent } from '@ghostfolio/ui/logo';
import {
ChangeDetectionStrategy,
Component,
computed,
CUSTOM_ELEMENTS_SCHEMA,
Input,
OnChanges
input
} from '@angular/core';
import { RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone';
@ -23,51 +23,53 @@ import { openOutline } from 'ionicons/icons';
styleUrls: ['./footer.component.scss'],
templateUrl: './footer.component.html'
})
export class GfFooterComponent implements OnChanges {
@Input() public info: InfoItem;
@Input() public user: User;
export class GfFooterComponent {
public readonly info = input.required<InfoItem | undefined>();
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
});
}
}

10
apps/client/src/app/components/header/header.component.html

@ -124,7 +124,7 @@
>About</a
>
</li>
@if (hasPermissionToAccessAssistant) {
@if (hasPermissionToAccessAssistant && user(); as user) {
<li class="d-inline-flex list-inline-item">
<button
#assistantTrigger="matMenuTrigger"
@ -144,7 +144,7 @@
class="no-max-width"
xPosition="before"
[overlapTrigger]="true"
(closed)="assistantElement()?.setIsOpen(false)"
(closed)="assistantElement().setIsOpen(false)"
>
<gf-assistant
#assistant
@ -156,7 +156,7 @@
hasPermissionToChangeDateRange()
"
[hasPermissionToChangeFilters]="hasPermissionToChangeFilters()"
[user]="user()"
[user]="user"
(closed)="closeAssistant()"
(dateRangeChanged)="onDateRangeChange($event)"
(filtersChanged)="onFiltersChanged($event)"
@ -191,7 +191,7 @@
<a class="d-flex" mat-menu-item [routerLink]="routerLinkPricing"
><span class="align-items-center d-flex"
><span>
@if (user().subscription.offer.isRenewal) {
@if (user()?.subscription?.offer?.isRenewal) {
<ng-container i18n>Renew Plan</ng-container>
} @else {
<ng-container i18n>Upgrade Plan</ng-container>
@ -203,7 +203,7 @@
></a>
<hr class="m-0" />
}
@if (user()?.access?.length > 0) {
@if ((user()?.access?.length ?? 0) > 0) {
<button mat-menu-item (click)="impersonateAccount(null)">
<span class="align-items-center d-flex">
<ion-icon

56
apps/client/src/app/components/home-holdings/home-holdings.component.ts

@ -1,5 +1,9 @@
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { DEFAULT_LOCALE } from '@ghostfolio/common/config';
import {
DEFAULT_CURRENCY,
DEFAULT_DATE_RANGE,
DEFAULT_LOCALE
} from '@ghostfolio/common/config';
import {
AssetProfileIdentifier,
PortfolioPosition,
@ -22,10 +26,12 @@ import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
computed,
CUSTOM_ELEMENTS_SCHEMA,
DestroyRef,
inject,
OnInit
OnInit,
signal
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatButtonModule } from '@angular/material/button';
@ -51,9 +57,22 @@ import { DeviceDetectorService } from 'ngx-device-detector';
export class GfHomeHoldingsComponent implements OnInit {
public static DEFAULT_HOLDINGS_VIEW_MODE: HoldingsViewMode = 'TABLE';
protected readonly DEFAULT_LOCALE = DEFAULT_LOCALE;
protected readonly baseCurrency = computed(() => {
return this.user()?.settings?.baseCurrency ?? DEFAULT_CURRENCY;
});
protected readonly colorScheme = computed(() => {
return this.user()?.settings?.colorScheme;
});
protected readonly dateRange = computed(() => {
return this.user()?.settings?.dateRange ?? DEFAULT_DATE_RANGE;
});
protected readonly deviceType = computed(() => {
return this.deviceDetectorService.deviceInfo().deviceType;
});
protected deviceType: string;
protected hasPermissionToAccessHoldingsChart: boolean;
protected hasPermissionToCreateActivity: boolean;
protected holdings: PortfolioPosition[] | undefined;
@ -78,9 +97,14 @@ export class GfHomeHoldingsComponent implements OnInit {
{ label: $localize`Closed`, value: 'CLOSED' }
];
protected isHoldingsViewModeToggleDisabled = true;
protected readonly locale = computed(() => {
return this.user()?.settings?.locale ?? DEFAULT_LOCALE;
});
protected readonly routerLinkPortfolioActivities =
internalRoutes.portfolio.subRoutes.activities.routerLink;
protected user: User;
protected readonly user = signal<User | null>(null);
private readonly changeDetectorRef = inject(ChangeDetectorRef);
private readonly dataService = inject(DataService);
@ -94,22 +118,22 @@ export class GfHomeHoldingsComponent implements OnInit {
}
public ngOnInit() {
this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType;
this.userService.stateChanged
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => {
if (state?.user) {
this.user = state.user;
const user = state.user;
this.user.set(user);
this.hasPermissionToAccessHoldingsChart = hasPermission(
this.user.permissions,
user.permissions,
permissions.accessHoldingsChart
);
this.hasPermissionToCreateActivity =
hasPermission(this.user.permissions, permissions.createActivity) &&
hasScope(this.user.scopes, scopes.activityCreate);
hasPermission(user.permissions, permissions.createActivity) &&
hasScope(user.scopes, scopes.activityCreate);
this.initialize();
}
@ -129,7 +153,7 @@ export class GfHomeHoldingsComponent implements OnInit {
.get(true)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((user) => {
this.user = user;
this.user.set(user);
this.changeDetectorRef.markForCheck();
});
@ -144,7 +168,7 @@ export class GfHomeHoldingsComponent implements OnInit {
protected onHoldingClicked({ dataSource, symbol }: AssetProfileIdentifier) {
if (dataSource && symbol) {
this.router.navigate([], {
void this.router.navigate([], {
queryParams: { dataSource, symbol, holdingDetailDialog: true }
});
}
@ -157,7 +181,7 @@ export class GfHomeHoldingsComponent implements OnInit {
return this.dataService.fetchPortfolioHoldings({
filters,
range: this.user?.settings?.dateRange
range: this.user()?.settings?.dateRange
});
}
@ -171,9 +195,9 @@ export class GfHomeHoldingsComponent implements OnInit {
this.isHoldingsViewModeToggleDisabled = false;
this.holdingsViewMode =
this.deviceType === 'mobile'
this.deviceType() === 'mobile'
? GfHomeHoldingsComponent.DEFAULT_HOLDINGS_VIEW_MODE
: (this.user?.settings?.holdingsViewMode ??
: (this.user()?.settings?.holdingsViewMode ??
GfHomeHoldingsComponent.DEFAULT_HOLDINGS_VIEW_MODE);
} else {
this.holdingsViewMode =

10
apps/client/src/app/components/home-holdings/home-holdings.html

@ -27,18 +27,18 @@
<gf-treemap-chart
class="mt-3"
cursor="pointer"
[baseCurrency]="user?.settings?.baseCurrency"
[colorScheme]="user?.settings?.colorScheme"
[dateRange]="user?.settings?.dateRange"
[baseCurrency]="baseCurrency()"
[colorScheme]="colorScheme()"
[dateRange]="dateRange()"
[holdings]="holdings"
[locale]="user?.settings?.locale"
[locale]="locale()"
(treemapChartClicked)="onHoldingClicked($event)"
/>
}
<div [class.d-none]="holdingsViewMode !== 'TABLE'">
<gf-holdings-table
[holdings]="holdings"
[locale]="user?.settings?.locale ?? DEFAULT_LOCALE"
[locale]="locale()"
(holdingClicked)="onHoldingClicked($event)"
/>
@if (hasPermissionToCreateActivity && (holdings?.length ?? 0) > 0) {

8
apps/client/src/app/components/home-overview/home-overview.component.ts

@ -53,7 +53,6 @@ import { DeviceDetectorService } from 'ngx-device-detector';
templateUrl: './home-overview.html'
})
export class GfHomeOverviewComponent implements OnInit {
protected readonly DEFAULT_LOCALE = DEFAULT_LOCALE;
protected readonly errors = signal<AssetProfileIdentifier[]>([]);
protected readonly hasImpersonationId = signal(false);
protected readonly historicalDataItems = signal<LineChartItem[] | null>(null);
@ -61,6 +60,11 @@ export class GfHomeOverviewComponent implements OnInit {
undefined
);
protected readonly isLoadingPerformance = signal(true);
protected readonly locale = computed(() => {
return this.user()?.settings?.locale ?? DEFAULT_LOCALE;
});
protected readonly performance = signal<PortfolioPerformance | null>(null);
protected readonly performanceLabel = $localize`Performance`;
protected readonly precision = signal(2);
@ -147,7 +151,7 @@ export class GfHomeOverviewComponent implements OnInit {
protected onHoldingClicked({ dataSource, symbol }: AssetProfileIdentifier) {
if (dataSource && symbol) {
this.router.navigate([], {
void this.router.navigate([], {
queryParams: { dataSource, symbol, holdingDetailDialog: true }
});
}

13
apps/client/src/app/components/home-overview/home-overview.html

@ -11,7 +11,10 @@
<h4 i18n>Welcome to Ghostfolio</h4>
<p i18n>Ready to take control of your personal finances?</p>
<ol class="font-weight-bold">
<li class="mb-2" [class.text-muted]="user()?.accounts?.length > 1">
<li
class="mb-2"
[class.text-muted]="(user()?.accounts?.length ?? 0) > 1"
>
<a class="d-block" [routerLink]="routerLinkAccounts"
><span i18n>Setup your accounts</span><br />
<span class="font-weight-normal" i18n
@ -48,7 +51,7 @@
>
<ng-container i18n>Setup accounts</ng-container>
</a>
} @else if (user()?.accounts?.length > 1) {
} @else if ((user()?.accounts?.length ?? 0) > 1) {
<a
color="primary"
mat-flat-button
@ -73,7 +76,7 @@
[historicalDataItems]="historicalDataItems()"
[isAnimated]="user()?.settings?.dateRange === '1d' ? false : true"
[label]="performanceLabel"
[locale]="user()?.settings?.locale"
[locale]="locale()"
[showGradient]="true"
[showLoader]="false"
[showXAxis]="false"
@ -85,7 +88,7 @@
<gf-portfolio-performance
[errors]="errors()"
[isLoading]="isLoadingPerformance()"
[locale]="user()?.settings?.locale"
[locale]="locale()"
[performance]="performance()"
[precision]="precision()"
[showDetails]="showDetails()"
@ -107,7 +110,7 @@
<gf-holdings-table
mode="simple"
[holdings]="holdings()"
[locale]="user()?.settings?.locale ?? DEFAULT_LOCALE"
[locale]="locale()"
(holdingClicked)="onHoldingClicked($event)"
/>
</mat-card-content>

23
apps/client/src/app/components/investment-chart/investment-chart.component.ts

@ -113,10 +113,15 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
}
private initialize() {
// Create a clone
const isBarChart = !!this.groupBy;
const benchmarkColorRgb = isBarChart ? primaryColorRgb : secondaryColorRgb;
// Create clones of the input data
this.investments = this.benchmarkDataItems.map((item) =>
Object.assign({}, item)
);
this.values = this.historicalDataItems.map((item) =>
Object.assign({}, item)
);
@ -127,9 +132,9 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
}),
datasets: [
{
backgroundColor: `rgb(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b})`,
borderColor: `rgb(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b})`,
borderWidth: this.groupBy ? 0 : 1,
backgroundColor: `rgb(${benchmarkColorRgb.r}, ${benchmarkColorRgb.g}, ${benchmarkColorRgb.b})`,
borderColor: `rgb(${benchmarkColorRgb.r}, ${benchmarkColorRgb.g}, ${benchmarkColorRgb.b})`,
borderWidth: isBarChart ? 0 : 1,
data: this.investments.map(({ date, investment }) => {
return {
x: parseDate(date)?.getTime() ?? null,
@ -141,7 +146,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
borderColor: (context) =>
this.isInFuture(
context,
`rgba(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b}, 0.67)`
`rgba(${benchmarkColorRgb.r}, ${benchmarkColorRgb.g}, ${benchmarkColorRgb.b}, 0.67)`
),
borderDash: (context) => this.isInFuture(context, [2, 2])
},
@ -200,10 +205,10 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
annotations: {
savingsRate: this.savingsRate
? {
borderColor: `rgba(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b}, 0.75)`,
borderColor: `rgba(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b}, 0.75)`,
borderWidth: 1,
label: {
backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
backgroundColor: `rgb(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b})`,
borderRadius: 2,
color: 'white',
content: $localize`Savings Rate`,
@ -234,7 +239,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
responsive: true,
scales: {
x: getTimeAxisOptions({
borderWidth: this.groupBy ? 0 : 1,
borderWidth: isBarChart ? 0 : 1,
colorScheme: this.colorScheme,
locale: this.locale
}),
@ -250,7 +255,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
plugins: [
getVerticalHoverLinePlugin(this.chartCanvas(), this.colorScheme)
],
type: this.groupBy ? 'bar' : 'line'
type: isBarChart ? 'bar' : 'line'
}
);
}

14
apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts

@ -35,9 +35,9 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
})
export class GfPortfolioPerformanceComponent {
public readonly errors = input<ResponseError['errors']>();
public readonly isLoading = input<boolean>();
public readonly isLoading = input<boolean>(false);
public readonly locale = input<string>(getLocale());
public readonly performance = input.required<PortfolioPerformance>();
public readonly performance = input.required<PortfolioPerformance | null>();
public readonly precision = input.required<number, number>({
transform: (value) => {
return value >= 0 ? value : 2;
@ -60,17 +60,19 @@ export class GfPortfolioPerformanceComponent {
this.value().nativeElement.innerHTML = '';
}
} else {
if (isNumber(this.performance().currentValueInBaseCurrency)) {
new CountUp('value', this.performance().currentValueInBaseCurrency, {
const performance = this.performance();
if (isNumber(performance?.currentValueInBaseCurrency)) {
new CountUp('value', performance.currentValueInBaseCurrency, {
decimal: getNumberFormatDecimal(this.locale()),
decimalPlaces: this.precision(),
duration: 1,
separator: getNumberFormatGroup(this.locale())
}).start();
} else if (this.showDetails() === false) {
} else if (this.showDetails() === false && performance) {
new CountUp(
'value',
this.performance().netPerformancePercentageWithCurrencyEffect * 100,
performance.netPerformancePercentageWithCurrencyEffect * 100,
{
decimal: getNumberFormatDecimal(this.locale()),
decimalPlaces: 2,

4
apps/client/src/app/components/user-account-membership/user-account-membership.html

@ -3,7 +3,9 @@
<div class="col">
<div class="align-items-center d-flex flex-column">
<gf-membership-card
[expiresAt]="user?.subscription?.expiresAt | date: defaultDateFormat"
[expiresAt]="
(user?.subscription?.expiresAt | date: defaultDateFormat) ?? ''
"
[hasPermissionToCreateApiKey]="hasPermissionToCreateApiKey"
[hover3d]="true"
[name]="user?.subscription?.type"

8
apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html

@ -43,7 +43,7 @@
}}</span>
</mat-form-field>
</div>
<div [class.d-none]="platforms?.length < 1">
<div [class.d-none]="platforms.length < 1">
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Platform</mat-label>
@if (selectedPlatform) {
@ -51,8 +51,8 @@
class="ml-3"
matPrefix
[hasPlaceholder]="true"
[tooltip]="selectedPlatform?.name"
[url]="selectedPlatform?.url"
[tooltip]="selectedPlatform.name ?? ''"
[url]="selectedPlatform.url"
/>
}
<input
@ -73,7 +73,7 @@
<gf-entity-logo
class="mr-1"
[hasPlaceholder]="true"
[tooltip]="platformEntry.name"
[tooltip]="platformEntry.name ?? ''"
[url]="platformEntry.url"
/>
<span>{{ platformEntry.name }}</span>

44
apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts

@ -5,6 +5,7 @@ import {
DEFAULT_DATE_RANGE,
NUMERICAL_PRECISION_THRESHOLD_6_FIGURES
} from '@ghostfolio/common/config';
import { SubscriptionType } from '@ghostfolio/common/enums';
import { canOpenHoldingDetail } from '@ghostfolio/common/helper';
import {
InvestmentItem,
@ -93,6 +94,7 @@ export class GfAnalysisPageComponent implements OnInit {
protected isLoadingDividendTimelineChart: boolean;
protected isLoadingInvestmentChart: boolean;
protected isLoadingInvestmentTimelineChart: boolean;
protected isLoadingPerformanceTimelineChart: boolean;
protected isLoadingPortfolioPrompt: boolean;
protected readonly mode = signal<GroupBy>('month');
protected readonly modeOptions: ToggleOption<GroupBy>[] = [
@ -103,7 +105,9 @@ export class GfAnalysisPageComponent implements OnInit {
protected readonly PerformanceCalculationType = PerformanceCalculationType;
protected performanceDataItems: LineChartItem[];
protected performanceDataItemsInPercentage: LineChartItem[];
protected readonly portfolioEvolutionDataLabel = $localize`Investment`;
protected performancePercentagesByYear: InvestmentItem[];
protected readonly performanceTimelineDataLabel = $localize`Net Performance`;
protected readonly portfolioEvolutionDataLabel = $localize`Invested Capital`;
protected precision = 2;
protected savingsRatePerMonth: number | undefined;
protected streaks: PortfolioInvestmentsResponse['streaks'];
@ -312,6 +316,37 @@ export class GfAnalysisPageComponent implements OnInit {
);
}
private fetchPerformanceByYear() {
this.isLoadingPerformanceTimelineChart = true;
this.dataService
.fetchPortfolioPerformance({
filters: this.userService.getFilters(),
groupBy: 'year',
range: this.user?.settings?.dateRange ?? DEFAULT_DATE_RANGE
})
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ chart }) => {
this.performancePercentagesByYear = [];
for (const {
date,
netPerformanceInPercentageWithCurrencyEffect
} of chart ?? []) {
if (isNumber(netPerformanceInPercentageWithCurrencyEffect)) {
this.performancePercentagesByYear.push({
date,
investment: netPerformanceInPercentageWithCurrencyEffect
});
}
}
this.isLoadingPerformanceTimelineChart = false;
this.changeDetectorRef.markForCheck();
});
}
private update() {
this.isLoadingInvestmentChart = true;
@ -420,6 +455,13 @@ export class GfAnalysisPageComponent implements OnInit {
this.fetchDividendsAndInvestments();
if (
this.user?.settings?.isExperimentalFeatures &&
this.user?.subscription?.type !== SubscriptionType.Basic
) {
this.fetchPerformanceByYear();
}
this.changeDetectorRef.markForCheck();
}

29
apps/client/src/app/pages/portfolio/analysis/analysis-page.html

@ -430,6 +430,35 @@
</div>
</div>
@if (user?.settings?.isExperimentalFeatures) {
<div class="mb-5 row">
<div class="col-lg">
<div class="align-items-center d-flex mb-4">
<div
class="align-items-center d-flex flex-grow-1 h5 mb-0 text-truncate"
>
<span i18n>Performance Timeline</span>
@if (user?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" />
}
</div>
</div>
<div class="chart-container">
<gf-investment-chart
class="h-100"
groupBy="year"
[benchmarkDataItems]="performancePercentagesByYear"
[benchmarkDataLabel]="performanceTimelineDataLabel"
[colorScheme]="user?.settings?.colorScheme"
[isInPercentage]="true"
[isLoading]="isLoadingPerformanceTimelineChart"
[locale]="user?.settings?.locale"
/>
</div>
</div>
</div>
}
<div class="mb-5 row">
<div class="col-lg">
<div class="align-items-center d-flex mb-4">

7
apps/client/src/app/util/date.util.ts

@ -10,16 +10,17 @@ export function formatRelativeTime({
date,
language
}: {
date: string;
date: Date | string | null | undefined;
language?: string;
}) {
if (date) {
const distanceString = formatDistanceToNowStrict(parseISO(date), {
const parsedDate = typeof date === 'string' ? parseISO(date) : date;
const distanceString = formatDistanceToNowStrict(parsedDate, {
addSuffix: true,
locale: getDateFnsLocale(language)
});
return Math.abs(differenceInSeconds(parseISO(date), new Date())) < 60
return Math.abs(differenceInSeconds(parsedDate, new Date())) < 60
? $localize`just now`
: distanceString;
}

54
apps/client/src/locales/messages.zh.xlf

@ -737,7 +737,7 @@
</trans-unit>
<trans-unit id="5611965261696422586" datatype="html">
<source>and is driven by the efforts of its <x id="START_LINK" ctype="x-a" equiv-text="&lt;a href=&quot;https://github.com/ghostfolio/ghostfolio/graphs/contributors&quot; i18n-title title=&quot;Contributors to Ghostfolio&quot; &gt;"/>contributors<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/></source>
<target state="translated">并且得益于其 <x id="START_LINK" ctype="x-a" equiv-text="&lt;a href=&quot;https://github.com/ghostfolio/ghostfolio/graphs/contributors&quot; title=&quot;Contributors to Ghostfolio&quot; &gt;"/>贡献者<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/></target>
<target state="translated">发起,并在各位 <x id="START_LINK" ctype="x-a" equiv-text="&lt;a href=&quot;https://github.com/ghostfolio/ghostfolio/graphs/contributors&quot; title=&quot;Contributors to Ghostfolio&quot; &gt;"/>贡献者<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/> 的共同支持下持续发展</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/about/overview/about-overview-page.html</context>
<context context-type="linenumber">50</context>
@ -1061,7 +1061,7 @@
</trans-unit>
<trans-unit id="5298486223740169185" datatype="html">
<source>By <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="INTERPOLATION" equiv-text="{{ retirementDateLabel }}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/>, this is projected to increase to <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot; hasPermissionToShowValues ? withdrawalRatePerYearProjected?.toNumber() : null &quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isPercent]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [precision]=&quot;2&quot; [value]=&quot;user?.settings?.annualInterestRate / 100&quot; /&gt;"/> per year<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/> or <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE_1" ctype="x-gf_value_1" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot; hasPermissionToShowValues ? withdrawalRatePerMonthProjected?.toNumber() : null &quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isPercent]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [precision]=&quot;2&quot; [value]=&quot;user?.settings?.annualInterestRate / 100&quot; /&gt;"/> per month<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/>, assuming a <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE_2" ctype="x-gf_value_2" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isPercent]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [precision]=&quot;2&quot; [value]=&quot;user?.settings?.annualInterestRate / 100&quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isPercent]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [precision]=&quot;2&quot; [value]=&quot;user?.settings?.annualInterestRate / 100&quot; /&gt;"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/> annual interest rate.</source>
<target state="new">到<x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="INTERPOLATION" equiv-text="{{ retirementDateLabel }}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/>,预计这将增长至每年<x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot;withdrawalRatePerYearProjected?.toNumber()&quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isPercent]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [precision]=&quot;2&quot; [value]=&quot;user?.settings?.annualInterestRate / 100&quot; /&gt;"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/>或每月<x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE_1" ctype="x-gf_value_1" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot;withdrawalRatePerMonthProjected?.toNumber()&quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isPercent]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [precision]=&quot;2&quot; [value]=&quot;user?.settings?.annualInterestRate / 100&quot; /&gt;"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/>,假设年利率为<x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE_2" ctype="x-gf_value_2" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isPercent]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [precision]=&quot;2&quot; [value]=&quot;user?.settings?.annualInterestRate / 100&quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isPercent]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [precision]=&quot;2&quot; [value]=&quot;user?.settings?.annualInterestRate / 100&quot; /&gt;"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/>。</target>
<target state="translated">截至 <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="INTERPOLATION" equiv-text="{{ retirementDateLabel }}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/>,预计将增长至每年 <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot;withdrawalRatePerYearProjected?.toNumber()&quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isPercent]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [precision]=&quot;2&quot; [value]=&quot;user?.settings?.annualInterestRate / 100&quot; /&gt;"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/> 或每月 <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE_1" ctype="x-gf_value_1" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot;withdrawalRatePerMonthProjected?.toNumber()&quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isPercent]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [precision]=&quot;2&quot; [value]=&quot;user?.settings?.annualInterestRate / 100&quot; /&gt;"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/>,假设年利率为 <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE_2" ctype="x-gf_value_2" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isPercent]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [precision]=&quot;2&quot; [value]=&quot;user?.settings?.annualInterestRate / 100&quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isPercent]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [precision]=&quot;2&quot; [value]=&quot;user?.settings?.annualInterestRate / 100&quot; /&gt;"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/>。</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">144</context>
@ -1177,7 +1177,7 @@
</trans-unit>
<trans-unit id="5918319622596548216" datatype="html">
<source>Financial Advisor</source>
<target state="new">Financial Advisor</target>
<target state="translated">财务顾问</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">110</context>
@ -1845,7 +1845,7 @@
</trans-unit>
<trans-unit id="5289957034780335504" datatype="html">
<source>The source code is fully available as <x id="START_LINK" ctype="x-a" equiv-text="&lt;a href=&quot;https://github.com/ghostfolio/ghostfolio&quot; i18n-title title=&quot;Find Ghostfolio on GitHub&quot; &gt;"/>open source software<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/> (OSS) under the <x id="START_LINK_1" equiv-text="&lt;a href=&quot;https://www.gnu.org/licenses/agpl-3.0.html&quot; title=&quot;GNU Affero General Public License&quot; &gt;"/>AGPL-3.0 license<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/></source>
<target state="translated">源代码完全可用,作为<x id="START_LINK" ctype="x-a" equiv-text="&lt;a href=&quot;https://github.com/ghostfolio/ghostfolio&quot; title=&quot;Find Ghostfolio on GitHub&quot; &gt;"/>开源软件<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/> (OSS),遵循<x id="START_LINK_1" equiv-text="&lt;a href=&quot;https://www.gnu.org/licenses/agpl-3.0.html&quot; title=&quot;GNU Affero General Public License&quot; &gt;"/>AGPL-3.0许可证<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/></target>
<target state="translated">源代码作为<x id="START_LINK" ctype="x-a" equiv-text="&lt;a href=&quot;https://github.com/ghostfolio/ghostfolio&quot; title=&quot;Find Ghostfolio on GitHub&quot; &gt;"/>开源软件<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/> (OSS) 完全开放,遵循 <x id="START_LINK_1" equiv-text="&lt;a href=&quot;https://www.gnu.org/licenses/agpl-3.0.html&quot; title=&quot;GNU Affero General Public License&quot; &gt;"/>AGPL-3.0 许可证<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/></target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/about/overview/about-overview-page.html</context>
<context context-type="linenumber">16</context>
@ -2693,7 +2693,7 @@
</trans-unit>
<trans-unit id="3885497195825665706" datatype="html">
<source>Next</source>
<target state="new">Next</target>
<target state="translated">下一个</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/resources/personal-finance-tools/product-page.html</context>
<context context-type="linenumber">406</context>
@ -4369,7 +4369,7 @@
</trans-unit>
<trans-unit id="5176578390795646240" datatype="html">
<source>thereof Dividend Performance</source>
<target state="new">thereof Dividend Performance</target>
<target state="translated">其中:股息绩效</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">178</context>
@ -4449,7 +4449,7 @@
</trans-unit>
<trans-unit id="7500665368930738879" datatype="html">
<source>or start a discussion at</source>
<target state="translated">或在以下位置开始讨论</target>
<target state="translated">或发起讨论:</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/about/overview/about-overview-page.html</context>
<context context-type="linenumber">98</context>
@ -4793,7 +4793,7 @@
</trans-unit>
<trans-unit id="7771702342975481710" datatype="html">
<source>Add your first activity</source>
<target state="new">Add your first activity</target>
<target state="translated">添加您的第一个活动</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/no-activities-info/no-activities-info.component.html</context>
<context context-type="linenumber">12</context>
@ -5234,7 +5234,7 @@
</trans-unit>
<trans-unit id="3893742511841789716" datatype="html">
<source>Discover Open Source Alternatives for Personal Finance Tools</source>
<target state="translated">发现个人理财工具的开源替代品</target>
<target state="translated">探索个人理财工具的开源替代方案</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/resources/personal-finance-tools/personal-finance-tools-page.html</context>
<context context-type="linenumber">5</context>
@ -5242,7 +5242,7 @@
</trans-unit>
<trans-unit id="9040028765832531851" datatype="html">
<source>This overview page features a curated collection of personal finance tools compared to the open source alternative <x id="START_LINK" ctype="x-a" equiv-text="&lt;a [routerLink]=&quot;routerLinkAbout&quot;&gt;"/>Ghostfolio<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a&gt;"/>. If you value transparency, data privacy, and community collaboration, Ghostfolio provides an excellent opportunity to take control of your financial management.</source>
<target state="translated">此概述页面包含与开源替代方案相比的精选个人理财工具集合<x id="START_LINK" ctype="x-a" equiv-text="&lt;a [routerLink]=&quot;routerLinkAbout&quot;&gt;"/>Ghostfolio<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a&gt;"/>。如果您重视透明度、数据隐私和社区协作,Ghostfolio 提供了一个绝佳的机会来控制您的财务管理。</target>
<target state="translated">本页面精选了一系列个人理财工具,并将其与开源替代方案 <x id="START_LINK" ctype="x-a" equiv-text="&lt;a [routerLink]=&quot;routerLinkAbout&quot;&gt;"/>Ghostfolio<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a&gt;"/> 进行对比。如果您看重透明度、数据隐私与社区协作,Ghostfolio 将是您掌控个人财务的绝佳选择。</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/resources/personal-finance-tools/personal-finance-tools-page.html</context>
<context context-type="linenumber">9</context>
@ -5250,7 +5250,7 @@
</trans-unit>
<trans-unit id="1509032395181307486" datatype="html">
<source>Explore the links below to compare a variety of personal finance tools with Ghostfolio.</source>
<target state="translated">浏览下面的链接,将各种个人理财工具与 Ghostfolio 进行比较。</target>
<target state="translated">点击下方链接,深入了解各类个人理财工具与 Ghostfolio 的详细对比。</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/resources/personal-finance-tools/personal-finance-tools-page.html</context>
<context context-type="linenumber">17</context>
@ -5258,7 +5258,7 @@
</trans-unit>
<trans-unit id="1362943485319900492" datatype="html">
<source>Open Source Alternative to <x id="INTERPOLATION" equiv-text="{{ personalFinanceTool.name }}"/></source>
<target state="translated"><x id="INTERPOLATION" equiv-text="{{ personalFinanceTool.name }}"/> 的开源替代品</target>
<target state="translated"><x id="INTERPOLATION" equiv-text="{{ personalFinanceTool.name }}"/> 的开源替代方案</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/resources/personal-finance-tools/personal-finance-tools-page.html</context>
<context context-type="linenumber">43</context>
@ -5274,7 +5274,7 @@
</trans-unit>
<trans-unit id="5100352603258735382" datatype="html">
<source>Are you looking for an open source alternative to <x id="INTERPOLATION" equiv-text="{{ product2().name }}"/>? <x id="START_LINK" ctype="x-a" equiv-text="&lt;a [routerLink]=&quot;routerLinkAbout&quot;&gt;"/>Ghostfolio<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/> is a powerful portfolio management tool that provides individuals with a comprehensive platform to track, analyze, and optimize their investments. Whether you are an experienced investor or just starting out, Ghostfolio offers an intuitive user interface and a <x id="START_LINK_1" equiv-text="&lt;a [routerLink]=&quot;routerLinkFeatures&quot; &gt;"/>wide range of functionalities<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/> to help you make informed decisions and take control of your financial future.</source>
<target state="translated">您是否正在寻找开源替代方案<x id="INTERPOLATION" equiv-text="{{ product2().name }}"/>?<x id="START_LINK" ctype="x-a" equiv-text="&lt;a [routerLink]=&quot;routerLinkAbout&quot;&gt;"/>幽灵作品集<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/>是一个强大的投资组合管理工具,为个人提供一个全面的平台来跟踪、分析和优化他们的投资。无论您是经验丰富的投资者还是刚刚起步的投资者,Ghostfolio 都提供直观的用户界面和<x id="START_LINK_1" equiv-text="&lt;a [routerLink]=&quot;routerLinkFeatures&quot; &gt;"/>广泛的功能<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/>帮助您做出明智的决定并掌控您的财务未来。</target>
<target state="translated">您是否正在寻找 <x id="INTERPOLATION" equiv-text="{{ product2().name }}"/> 的开源替代方案?<x id="START_LINK" ctype="x-a" equiv-text="&lt;a [routerLink]=&quot;routerLinkAbout&quot;&gt;"/>Ghostfolio<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/> 是一款功能强大的投资组合管理工具,为个人投资者提供全面的资产跟踪、分析和优化平台。无论您是资深投资者还是初学者,Ghostfolio 都能提供直观的用户界面和<x id="START_LINK_1" equiv-text="&lt;a [routerLink]=&quot;routerLinkFeatures&quot; &gt;"/>丰富的功能<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/>,帮助您做出明智的决策并掌控自己的财务未来。</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/resources/personal-finance-tools/product-page.html</context>
<context context-type="linenumber">18</context>
@ -5290,7 +5290,7 @@
</trans-unit>
<trans-unit id="7314014538091101522" datatype="html">
<source>Ghostfolio is an open source software (OSS), providing a cost-effective alternative to <x id="INTERPOLATION" equiv-text="{{ product2().name }}"/> making it particularly suitable for individuals on a tight budget, such as those <x id="START_LINK" ctype="x-a" equiv-text="&lt;a href=&quot;../en/blog/2023/07/exploring-the-path-to-fire&quot; &gt;"/>pursuing Financial Independence, Retire Early (FIRE)<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/>. By leveraging the collective efforts of a community of developers and personal finance enthusiasts, Ghostfolio continuously enhances its capabilities, security, and user experience.</source>
<target state="translated">Ghostfolio 是一款开源软件 (OSS),提供了一种经济高效的替代方案<x id="INTERPOLATION" equiv-text="{{ product2().name }}"/>使其特别适合预算紧张的个人,例如<x id="START_LINK" ctype="x-a" equiv-text="&lt;a href=&quot;../en/blog/2023/07/exploring-the-path-to-fire&quot; &gt;"/>追求财务独立,提前退休(FIRE)<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/> 。通过利用开发者社区和个人理财爱好者的集体努力,Ghostfolio 不断增强其功能、安全性和用户体验。</target>
<target state="translated">Ghostfolio 是一款开源软件 (OSS),为 <x id="INTERPOLATION" equiv-text="{{ product2().name }}"/> 提供了高性价比的替代方案,特别适合预算有限的用户,例如<x id="START_LINK" ctype="x-a" equiv-text="&lt;a href=&quot;../en/blog/2023/07/exploring-the-path-to-fire&quot; &gt;"/>追求财务独立、提前退休(FIRE)的人群<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/>。凝聚开发者社区与个人理财爱好者的共同力量,Ghostfolio 持续提升其功能、安全性与用户体验。</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/resources/personal-finance-tools/product-page.html</context>
<context context-type="linenumber">32</context>
@ -5298,7 +5298,7 @@
</trans-unit>
<trans-unit id="2315631726271602419" datatype="html">
<source>Let’s dive deeper into the detailed Ghostfolio vs <x id="INTERPOLATION" equiv-text="{{ product2().name }}"/> comparison table below to gain a thorough understanding of how Ghostfolio positions itself relative to <x id="INTERPOLATION" equiv-text="{{ product2().name }}"/>. We will explore various aspects such as features, data privacy, pricing, and more, allowing you to make a well-informed choice for your personal requirements.</source>
<target state="translated">让我们更深入地了解 Ghostfolio 与<x id="INTERPOLATION" equiv-text="{{ product2().name }}"/>下面的比较表可帮助您全面了解 Ghostfolio 相对于其他产品的定位<x id="INTERPOLATION" equiv-text="{{ product2().name }}"/>。我们将探讨功能、数据隐私、定价等各个方面,让您根据个人需求做出明智的选择。</target>
<target state="translated">让我们通过下方详细的 Ghostfolio 与 <x id="INTERPOLATION" equiv-text="{{ product2().name }}"/> 对比表,深入了解 Ghostfolio 相比于 <x id="INTERPOLATION" equiv-text="{{ product2().name }}"/> 的自身定位。我们将探讨功能、数据隐私、定价等各个方面,帮助您根据个人需求做出明智的选择。</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/resources/personal-finance-tools/product-page.html</context>
<context context-type="linenumber">43</context>
@ -5306,7 +5306,7 @@
</trans-unit>
<trans-unit id="4909535316439940790" datatype="html">
<source>Ghostfolio vs <x id="INTERPOLATION" equiv-text="{{ product2().name }}"/> comparison table</source>
<target state="translated">Ghostfolio vs<x id="INTERPOLATION" equiv-text="{{ product2().name }}"/>比较表</target>
<target state="translated">Ghostfolio 与 <x id="INTERPOLATION" equiv-text="{{ product2().name }}"/> 对比表</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/resources/personal-finance-tools/product-page.html</context>
<context context-type="linenumber">54</context>
@ -6174,7 +6174,7 @@
</trans-unit>
<trans-unit id="1825717711340685842" datatype="html">
<source>Export <x id="INTERPOLATION" equiv-text="{{ (totalItems ?? 0) &gt; 1 ? totalItems : &apos;&apos; }}"/> <x id="ICU" equiv-text="{totalItems, plural, =1 {Activity} other {Activities}}"/></source>
<target state="new">导出 <x id="INTERPOLATION" equiv-text="{{ totalItems &gt; 1 ? totalItems : &apos;&apos; }}"/> <x id="ICU" equiv-text="{totalItems, plural, =1 {活动} other {活动}}"/></target>
<target state="translated">导出 <x id="INTERPOLATION" equiv-text="{{ totalItems &gt; 1 ? totalItems : &apos;&apos; }}"/> <x id="ICU" equiv-text="{totalItems, plural, =1 {活动} other {项活动}}"/></target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.html</context>
<context context-type="linenumber">72</context>
@ -6374,7 +6374,7 @@
</trans-unit>
<trans-unit id="5552246967241337741" datatype="html">
<source>If you retire today, you would be able to withdraw <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot; hasPermissionToShowValues ? withdrawalRatePerYear?.toNumber() : null &quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot; hasPermissionToShowValues ? fireWealth?.today.valueInBaseCurrency : null &quot; /&gt;"/> per year<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/> or <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE_1" ctype="x-gf_value_1" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot; hasPermissionToShowValues ? withdrawalRatePerMonth?.toNumber() : null &quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot; hasPermissionToShowValues ? fireWealth?.today.valueInBaseCurrency : null &quot; /&gt;"/> per month<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/>, based on your total assets of <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE_2" ctype="x-gf_value_2" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot; hasPermissionToShowValues ? fireWealth?.today.valueInBaseCurrency : null &quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot; hasPermissionToShowValues ? fireWealth?.today.valueInBaseCurrency : null &quot; /&gt;"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/> and a safe withdrawal rate (SWR) of</source>
<target state="new">如果您今天退休,您将能够提取 <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot;withdrawalRatePerYear?.toNumber()&quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot;fireWealth?.today.valueInBaseCurrency&quot; /&gt;"/> 每年<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/> 或 <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE_1" ctype="x-gf_value_1" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot;withdrawalRatePerMonth?.toNumber()&quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot;fireWealth?.today.valueInBaseCurrency&quot; /&gt;"/> 每月<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/>基于您总资产的 <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE_2" ctype="x-gf_value_2" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot;fireWealth?.today.valueInBaseCurrency&quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot;fireWealth?.today.valueInBaseCurrency&quot; /&gt;"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/> 和安全取款率 (SWR) 为</target>
<target state="translated">如果您今天退休,您将能够每年提取 <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot;withdrawalRatePerYear?.toNumber()&quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot;fireWealth?.today.valueInBaseCurrency&quot; /&gt;"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/> 或每月提取 <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE_1" ctype="x-gf_value_1" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot;withdrawalRatePerMonth?.toNumber()&quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot;fireWealth?.today.valueInBaseCurrency&quot; /&gt;"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/>,基于您 <x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;font-weight-bold&quot; &gt;"/><x id="START_TAG_GF_VALUE_2" ctype="x-gf_value_2" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot;fireWealth?.today.valueInBaseCurrency&quot; /&gt;"/><x id="CLOSE_TAG_GF_VALUE" ctype="x-gf_value" equiv-text="&lt;gf-value class=&quot;d-inline-block&quot; [isCurrency]=&quot;true&quot; [locale]=&quot;user?.settings?.locale&quot; [unit]=&quot;user?.settings?.baseCurrency&quot; [value]=&quot;fireWealth?.today.valueInBaseCurrency&quot; /&gt;"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/> 的总资产,且安全取款率 (SWR) 为</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">69</context>
@ -6662,7 +6662,7 @@
</trans-unit>
<trans-unit id="3995811497329884593" datatype="html">
<source>Expires <x id="INTERPOLATION" equiv-text="{{ formatRelativeTime({ date: element.subscription.expiresAt, language: user?.settings?.language }) }}"/> (<x id="INTERPOLATION_1" equiv-text="{{ element.subscription.expiresAt | date: defaultDateFormat }}"/>)</source>
<target state="new">到期时间 <x id="INTERPOLATION" equiv-text="{{ formatDistanceToNow(element.subscription.expiresAt) }}"/> (<x id="INTERPOLATION_1" equiv-text="{{ element.subscription.expiresAt | date: defaultDateFormat }}"/>)</target>
<target state="translated"><x id="INTERPOLATION" equiv-text="{{ formatDistanceToNow(element.subscription.expiresAt) }}"/>到期 (<x id="INTERPOLATION_1" equiv-text="{{ element.subscription.expiresAt | date: defaultDateFormat }}"/>)</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-users/admin-users.html</context>
<context context-type="linenumber">34</context>
@ -6698,7 +6698,7 @@
</trans-unit>
<trans-unit id="251113330629316982" datatype="html">
<source>Date must be in the future.</source>
<target state="new">Date must be in the future.</target>
<target state="translated">日期必须在未来。</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html</context>
<context context-type="linenumber">70</context>
@ -7071,7 +7071,7 @@
</trans-unit>
<trans-unit id="4452427314943113135" datatype="html">
<source>Previous</source>
<target state="new">Previous</target>
<target state="translated">上一个</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/resources/personal-finance-tools/product-page.html</context>
<context context-type="linenumber">396</context>
@ -7395,7 +7395,7 @@
</trans-unit>
<trans-unit id="7803225195017107428" datatype="html">
<source>thereof Dividend</source>
<target state="new">thereof Dividend</target>
<target state="translated">其中:股息</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">102</context>
@ -8253,7 +8253,7 @@
</trans-unit>
<trans-unit id="1325095699053123251" datatype="html">
<source>The project has been initiated by</source>
<target state="translated">该项目发起于</target>
<target state="translated">该项目由</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/about/overview/about-overview-page.html</context>
<context context-type="linenumber">41</context>
@ -8514,7 +8514,7 @@
</trans-unit>
<trans-unit id="6278975207599702712" datatype="html">
<source>{VAR_PLURAL, plural, =1 {Activity} other {Activities}}</source>
<target state="translated">{VAR_PLURAL, plural, =1 {活动} other {活动}}</target>
<target state="translated">{VAR_PLURAL, plural, =1 {活动} other {项活动}}</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/activities-table/activities-table.component.html</context>
<context context-type="linenumber">73</context>
@ -8833,7 +8833,7 @@
</trans-unit>
<trans-unit id="7387635272539030076" datatype="html">
<source>new</source>
<target state="new">new</target>
<target state="translated">新</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/features/features-page.html</context>
<context context-type="linenumber">223</context>
@ -9006,7 +9006,7 @@
</trans-unit>
<trans-unit id="5199695670214400859" datatype="html">
<source>If you encounter a bug, would like to suggest an improvement or a new <x id="START_LINK" ctype="x-a" equiv-text="&lt;a [routerLink]=&quot;routerLinkFeatures&quot;&gt;"/>feature<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/>, please join the Ghostfolio <x id="START_LINK_1" equiv-text="&lt;a href=&quot;https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg&quot; i18n-title title=&quot;Join the Ghostfolio Slack community&quot; &gt;"/>Slack<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/> community, post to <x id="START_LINK_2" equiv-text="&lt;a href=&quot;https://x.com/ghostfolio_&quot; i18n-title title=&quot;Post to Ghostfolio on X (formerly Twitter)&quot; &gt;"/>@ghostfolio_<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/></source>
<target state="translated">如果您遇到错误,想要建议改进或新<x id="START_LINK" ctype="x-a" equiv-text="&lt;a [routerLink]=&quot;routerLinkFeatures&quot;&gt;"/>功能<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/>,请加入 Ghostfolio <x id="START_LINK_1" equiv-text="&lt;a href=&quot;https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg&quot; title=&quot;Join the Ghostfolio Slack community&quot; &gt;"/>Slack<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/> 社区,发布到 <x id="START_LINK_2" equiv-text="&lt;a href=&quot;https://x.com/ghostfolio_&quot; title=&quot;Post to Ghostfolio on X (formerly Twitter)&quot; &gt;"/>@ghostfolio_<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/></target>
<target state="translated">如果您遇到任何问题、想要提出改进建议或新<x id="START_LINK" ctype="x-a" equiv-text="&lt;a [routerLink]=&quot;routerLinkFeatures&quot;&gt;"/>功能<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/>需求,欢迎加入 Ghostfolio <x id="START_LINK_1" equiv-text="&lt;a href=&quot;https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg&quot; title=&quot;Join the Ghostfolio Slack community&quot; &gt;"/>Slack<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/> 社区,在 X 上联系 <x id="START_LINK_2" equiv-text="&lt;a href=&quot;https://x.com/ghostfolio_&quot; title=&quot;Post to Ghostfolio on X (formerly Twitter)&quot; &gt;"/>@ghostfolio_<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/></target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/about/overview/about-overview-page.html</context>
<context context-type="linenumber">71</context>
@ -9154,7 +9154,7 @@
</trans-unit>
<trans-unit id="5814001514445505929" datatype="html">
<source>Return on Average Investment (total return with dividends)</source>
<target state="new">Return on Average Investment (total return with dividends)</target>
<target state="translated">平均投资回报率(含股息总回报)</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">159</context>

2
libs/ui/src/lib/line-chart/line-chart.component.ts

@ -60,7 +60,7 @@ export class GfLineChartComponent
@Input() benchmarkLabel = '';
@Input() colorScheme?: ColorScheme;
@Input() currency?: string;
@Input() historicalDataItems: LineChartItem[];
@Input() historicalDataItems: LineChartItem[] | null;
@Input() isAnimated = false;
@Input() label: string;
@Input() locale = getLocale();

7
libs/ui/src/lib/services/data.service.ts

@ -728,14 +728,21 @@ export class DataService {
public fetchPortfolioPerformance({
filters,
groupBy,
range,
withExcludedAccounts = false
}: {
filters?: Filter[];
groupBy?: Extract<GroupBy, 'year'>;
range: DateRange;
withExcludedAccounts?: boolean;
}): Observable<PortfolioPerformanceResponse> {
let params = this.buildFiltersAsQueryParams({ filters });
if (groupBy) {
params = params.append('groupBy', groupBy);
}
params = params.append('range', range);
if (withExcludedAccounts) {

Loading…
Cancel
Save