diff --git a/CHANGELOG.md b/CHANGELOG.md index 51620435e..2a3283f75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Considered the availability of the date range selector in the assistant per view - Considered the availability of the filters in the assistant per view +- Optimized the portfolio calculations with smarter cloning of activities +- Integrated the add currency functionality into the market data section of the admin control panel +- Improved the language localization for German (`de`) +- Upgraded `prisma` from version `5.19.1` to `5.20.0` - Upgraded `webpack-bundle-analyzer` from version `4.10.1` to `4.10.2` ## 2.110.0 - 2024-09-24 diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts index fba0ead84..3b64cd185 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts @@ -180,10 +180,10 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { let valueAtStartDateWithCurrencyEffect: Big; // Clone orders to keep the original values in this.orders - let orders: PortfolioOrderItem[] = cloneDeep(this.activities).filter( - ({ SymbolProfile }) => { + let orders: PortfolioOrderItem[] = cloneDeep( + this.activities.filter(({ SymbolProfile }) => { return SymbolProfile.symbol === symbol; - } + }) ); if (orders.length <= 0) { diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.service.ts b/apps/client/src/app/components/admin-market-data/admin-market-data.service.ts index 006bacb95..b1d7154e9 100644 --- a/apps/client/src/app/components/admin-market-data/admin-market-data.service.ts +++ b/apps/client/src/app/components/admin-market-data/admin-market-data.service.ts @@ -2,7 +2,10 @@ import { ConfirmationDialogType } from '@ghostfolio/client/core/notification/con import { NotificationService } from '@ghostfolio/client/core/notification/notification.service'; import { AdminService } from '@ghostfolio/client/services/admin.service'; import { ghostfolioScraperApiSymbolPrefix } from '@ghostfolio/common/config'; -import { getCurrencyFromSymbol, isCurrency } from '@ghostfolio/common/helper'; +import { + getCurrencyFromSymbol, + isDerivedCurrency +} from '@ghostfolio/common/helper'; import { AssetProfileIdentifier, AdminMarketDataItem @@ -74,7 +77,7 @@ export class AdminMarketDataService { return ( activitiesCount === 0 && !isBenchmark && - !isCurrency(getCurrencyFromSymbol(symbol)) && + !isDerivedCurrency(getCurrencyFromSymbol(symbol)) && !symbol.startsWith(ghostfolioScraperApiSymbolPrefix) ); } diff --git a/apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.component.ts b/apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.component.ts index f0a47ad1b..422ac45a9 100644 --- a/apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.component.ts +++ b/apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.component.ts @@ -1,7 +1,10 @@ import { AdminService } from '@ghostfolio/client/services/admin.service'; +import { DataService } from '@ghostfolio/client/services/data.service'; +import { PROPERTY_CURRENCIES } from '@ghostfolio/common/config'; import { ChangeDetectionStrategy, + ChangeDetectorRef, Component, OnDestroy, OnInit @@ -15,6 +18,10 @@ import { Validators } from '@angular/forms'; import { MatDialogRef } from '@angular/material/dialog'; +import { uniq } from 'lodash'; +import { Subject, takeUntil } from 'rxjs'; + +import { CreateAssetProfileDialogMode } from './interfaces/interfaces'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, @@ -25,17 +32,29 @@ import { MatDialogRef } from '@angular/material/dialog'; }) export class CreateAssetProfileDialog implements OnInit, OnDestroy { public createAssetProfileForm: FormGroup; - public mode: 'auto' | 'manual'; + public mode: CreateAssetProfileDialogMode; + + private customCurrencies: string[]; + private unsubscribeSubject = new Subject(); public constructor( public readonly adminService: AdminService, + private readonly changeDetectorRef: ChangeDetectorRef, + private readonly dataService: DataService, public readonly dialogRef: MatDialogRef, public readonly formBuilder: FormBuilder ) {} public ngOnInit() { + this.initializeCustomCurrencies(); + this.createAssetProfileForm = this.formBuilder.group( { + addCurrency: new FormControl(null, [ + Validators.maxLength(3), + Validators.minLength(3), + Validators.required + ]), addSymbol: new FormControl(null, [Validators.required]), searchSymbol: new FormControl(null, [Validators.required]) }, @@ -51,34 +70,75 @@ export class CreateAssetProfileDialog implements OnInit, OnDestroy { this.dialogRef.close(); } - public onRadioChange(mode: 'auto' | 'manual') { + public onRadioChange(mode: CreateAssetProfileDialogMode) { this.mode = mode; } public onSubmit() { - this.mode === 'auto' - ? this.dialogRef.close({ - dataSource: - this.createAssetProfileForm.get('searchSymbol').value.dataSource, - symbol: this.createAssetProfileForm.get('searchSymbol').value.symbol + if (this.mode === 'auto') { + this.dialogRef.close({ + dataSource: + this.createAssetProfileForm.get('searchSymbol').value.dataSource, + symbol: this.createAssetProfileForm.get('searchSymbol').value.symbol + }); + } else if (this.mode === 'currency') { + const currency = this.createAssetProfileForm + .get('addCurrency') + .value.toUpperCase(); + + const currencies = uniq([...this.customCurrencies, currency]); + + this.dataService + .putAdminSetting(PROPERTY_CURRENCIES, { + value: JSON.stringify(currencies) }) - : this.dialogRef.close({ - dataSource: 'MANUAL', - symbol: this.createAssetProfileForm.get('addSymbol').value + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + this.dialogRef.close(); }); + } else if (this.mode === 'manual') { + this.dialogRef.close({ + dataSource: 'MANUAL', + symbol: this.createAssetProfileForm.get('addSymbol').value + }); + } } - public ngOnDestroy() {} + public get showCurrencyErrorMessage() { + const addCurrencyFormControl = + this.createAssetProfileForm.get('addCurrency'); + + if ( + addCurrencyFormControl.hasError('maxlength') || + addCurrencyFormControl.hasError('minlength') + ) { + return true; + } + + return false; + } + + public ngOnDestroy() { + this.unsubscribeSubject.next(); + this.unsubscribeSubject.complete(); + } private atLeastOneValid(control: AbstractControl): ValidationErrors { + const addCurrencyControl = control.get('addCurrency'); const addSymbolControl = control.get('addSymbol'); const searchSymbolControl = control.get('searchSymbol'); - if (addSymbolControl.valid && searchSymbolControl.valid) { + if ( + addCurrencyControl.valid && + addSymbolControl.valid && + searchSymbolControl.valid + ) { return { atLeastOneValid: true }; } if ( + addCurrencyControl.valid || + !addCurrencyControl || addSymbolControl.valid || !addSymbolControl || searchSymbolControl.valid || @@ -89,4 +149,15 @@ export class CreateAssetProfileDialog implements OnInit, OnDestroy { return { atLeastOneValid: true }; } + + private initializeCustomCurrencies() { + this.adminService + .fetchAdminData() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(({ settings }) => { + this.customCurrencies = settings[PROPERTY_CURRENCIES] as string[]; + + this.changeDetectorRef.markForCheck(); + }); + } } diff --git a/apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html b/apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html index 7c228941e..c60ca83b8 100644 --- a/apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html +++ b/apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.html @@ -17,6 +17,9 @@ + + + @@ -37,6 +40,16 @@ + } @else if (mode === 'currency') { +
+ + Currency + + @if (showCurrencyErrorMessage) { + Oops! Invalid currency. + } + +
}
diff --git a/apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/interfaces/interfaces.ts index 16be906c9..4cf24b554 100644 --- a/apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/interfaces/interfaces.ts +++ b/apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/interfaces/interfaces.ts @@ -2,3 +2,5 @@ export interface CreateAssetProfileDialogParams { deviceType: string; locale: string; } + +export type CreateAssetProfileDialogMode = 'auto' | 'currency' | 'manual'; diff --git a/apps/client/src/app/components/admin-overview/admin-overview.component.ts b/apps/client/src/app/components/admin-overview/admin-overview.component.ts index 9763f960f..15547bb6d 100644 --- a/apps/client/src/app/components/admin-overview/admin-overview.component.ts +++ b/apps/client/src/app/components/admin-overview/admin-overview.component.ts @@ -126,7 +126,10 @@ export class AdminOverviewComponent implements OnDestroy, OnInit { if (currency) { if (currency.length === 3) { - const currencies = uniq([...this.customCurrencies, currency]); + const currencies = uniq([ + ...this.customCurrencies, + currency.toUpperCase() + ]); this.putAdminSetting({ key: PROPERTY_CURRENCIES, value: currencies }); } else { this.notificationService.alert({ diff --git a/apps/client/src/app/pages/faq/self-hosting/self-hosting-page.html b/apps/client/src/app/pages/faq/self-hosting/self-hosting-page.html index 10cc23cb2..96da35ca9 100644 --- a/apps/client/src/app/pages/faq/self-hosting/self-hosting-page.html +++ b/apps/client/src/app/pages/faq/self-hosting/self-hosting-page.html @@ -52,8 +52,10 @@

  1. Go to the Admin Control panel
  2. -
  3. Click on the Add Currency button
  4. -
  5. Insert e.g. EUR in the prompt
  6. +
  7. Go to the Market Data section
  8. +
  9. Click on the + button
  10. +
  11. Switch to Add Currency
  12. +
  13. Insert e.g. EUR for Euro
diff --git a/apps/client/src/locales/messages.de.xlf b/apps/client/src/locales/messages.de.xlf index 340b2a562..b2fa6e9ad 100644 --- a/apps/client/src/locales/messages.de.xlf +++ b/apps/client/src/locales/messages.de.xlf @@ -2391,7 +2391,7 @@ Ghostfolio empowers you to keep track of your wealth. - Ghostfolio verschafft Ihnen den Überblick über Ihr Vermögen. + Ghostfolio verschafft dir den Überblick über dein Vermögen. apps/client/src/app/pages/public/public-page.html 215 @@ -4919,7 +4919,7 @@ Protect your assets. Refine your personal investment strategy. - Schützen Sie Ihr Vermögen. Optimieren Sie Ihre persönliche Anlagestrategie. + Schütze dein Vermögen. Optimiere deine persönliche Anlagestrategie. apps/client/src/app/pages/landing/landing-page.html 225 @@ -5728,7 +5728,7 @@ Choose or drop a file here - Wählen Sie eine Datei aus oder ziehen Sie sie hierhin + Wähle eine Datei aus oder ziehe sie hierhin apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html 84 @@ -5984,7 +5984,7 @@ Ghostfolio is a personal finance dashboard to keep track of your net worth including cash, stocks, ETFs and cryptocurrencies across multiple platforms. - Mit dem Finanz-Dashboard Ghostfolio können Sie Ihr Vermögen in Cash, Aktien, ETFs und Kryptowährungen über mehrere Finanzinstitute überwachen. + Mit dem Finanz-Dashboard Ghostfolio kannst du dein Vermögen in Cash, Aktien, ETFs und Kryptowährungen über mehrere Finanzinstitute überwachen. apps/client/src/app/pages/i18n/i18n-page.html 4 diff --git a/package-lock.json b/package-lock.json index 55a0dd609..b663482d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,7 +40,7 @@ "@nestjs/platform-express": "10.1.3", "@nestjs/schedule": "3.0.2", "@nestjs/serve-static": "4.0.0", - "@prisma/client": "5.19.1", + "@prisma/client": "5.20.0", "@simplewebauthn/browser": "9.0.1", "@simplewebauthn/server": "9.0.3", "@stripe/stripe-js": "3.5.0", @@ -84,7 +84,7 @@ "passport": "0.7.0", "passport-google-oauth20": "2.0.0", "passport-jwt": "4.0.1", - "prisma": "5.19.1", + "prisma": "5.20.0", "reflect-metadata": "0.1.13", "rxjs": "7.5.6", "stripe": "15.11.0", @@ -9646,9 +9646,9 @@ "dev": true }, "node_modules/@prisma/client": { - "version": "5.19.1", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.19.1.tgz", - "integrity": "sha512-x30GFguInsgt+4z5I4WbkZP2CGpotJMUXy+Gl/aaUjHn2o1DnLYNTA+q9XdYmAQZM8fIIkvUiA2NpgosM3fneg==", + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.20.0.tgz", + "integrity": "sha512-CLv55ZuMuUawMsxoqxGtLT3bEZoa2W8L3Qnp6rDIFWy+ZBrUcOFKdoeGPSnbBqxc3SkdxJrF+D1veN/WNynZYA==", "hasInstallScript": true, "license": "Apache-2.0", "engines": { @@ -9664,48 +9664,48 @@ } }, "node_modules/@prisma/debug": { - "version": "5.19.1", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.19.1.tgz", - "integrity": "sha512-lAG6A6QnG2AskAukIEucYJZxxcSqKsMK74ZFVfCTOM/7UiyJQi48v6TQ47d6qKG3LbMslqOvnTX25dj/qvclGg==", + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.20.0.tgz", + "integrity": "sha512-oCx79MJ4HSujokA8S1g0xgZUGybD4SyIOydoHMngFYiwEwYDQ5tBQkK5XoEHuwOYDKUOKRn/J0MEymckc4IgsQ==", "license": "Apache-2.0" }, "node_modules/@prisma/engines": { - "version": "5.19.1", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.19.1.tgz", - "integrity": "sha512-kR/PoxZDrfUmbbXqqb8SlBBgCjvGaJYMCOe189PEYzq9rKqitQ2fvT/VJ8PDSe8tTNxhc2KzsCfCAL+Iwm/7Cg==", + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.20.0.tgz", + "integrity": "sha512-DtqkP+hcZvPEbj8t8dK5df2b7d3B8GNauKqaddRRqQBBlgkbdhJkxhoJTrOowlS3vaRt2iMCkU0+CSNn0KhqAQ==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "5.19.1", - "@prisma/engines-version": "5.19.1-2.69d742ee20b815d88e17e54db4a2a7a3b30324e3", - "@prisma/fetch-engine": "5.19.1", - "@prisma/get-platform": "5.19.1" + "@prisma/debug": "5.20.0", + "@prisma/engines-version": "5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284", + "@prisma/fetch-engine": "5.20.0", + "@prisma/get-platform": "5.20.0" } }, "node_modules/@prisma/engines-version": { - "version": "5.19.1-2.69d742ee20b815d88e17e54db4a2a7a3b30324e3", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.19.1-2.69d742ee20b815d88e17e54db4a2a7a3b30324e3.tgz", - "integrity": "sha512-xR6rt+z5LnNqTP5BBc+8+ySgf4WNMimOKXRn6xfNRDSpHvbOEmd7+qAOmzCrddEc4Cp8nFC0txU14dstjH7FXA==", + "version": "5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284.tgz", + "integrity": "sha512-Lg8AS5lpi0auZe2Mn4gjuCg081UZf88k3cn0RCwHgR+6cyHHpttPZBElJTHf83ZGsRNAmVCZCfUGA57WB4u4JA==", "license": "Apache-2.0" }, "node_modules/@prisma/fetch-engine": { - "version": "5.19.1", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.19.1.tgz", - "integrity": "sha512-pCq74rtlOVJfn4pLmdJj+eI4P7w2dugOnnTXpRilP/6n5b2aZiA4ulJlE0ddCbTPkfHmOL9BfaRgA8o+1rfdHw==", + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.20.0.tgz", + "integrity": "sha512-JVcaPXC940wOGpCOwuqQRTz6I9SaBK0c1BAyC1pcz9xBi+dzFgUu3G/p9GV1FhFs9OKpfSpIhQfUJE9y00zhqw==", "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "5.19.1", - "@prisma/engines-version": "5.19.1-2.69d742ee20b815d88e17e54db4a2a7a3b30324e3", - "@prisma/get-platform": "5.19.1" + "@prisma/debug": "5.20.0", + "@prisma/engines-version": "5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284", + "@prisma/get-platform": "5.20.0" } }, "node_modules/@prisma/get-platform": { - "version": "5.19.1", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.19.1.tgz", - "integrity": "sha512-sCeoJ+7yt0UjnR+AXZL7vXlg5eNxaFOwC23h0KvW1YIXUoa7+W2ZcAUhoEQBmJTW4GrFqCuZ8YSP0mkDa4k3Zg==", + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.20.0.tgz", + "integrity": "sha512-8/+CehTZZNzJlvuryRgc77hZCWrUDYd/PmlZ7p2yNXtmf2Una4BWnTbak3us6WVdqoz5wmptk6IhsXdG2v5fmA==", "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "5.19.1" + "@prisma/debug": "5.20.0" } }, "node_modules/@redis/bloom": { @@ -28820,13 +28820,13 @@ } }, "node_modules/prisma": { - "version": "5.19.1", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.19.1.tgz", - "integrity": "sha512-c5K9MiDaa+VAAyh1OiYk76PXOme9s3E992D7kvvIOhCrNsBQfy2mP2QAQtX0WNj140IgG++12kwZpYB9iIydNQ==", + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.20.0.tgz", + "integrity": "sha512-6obb3ucKgAnsGS9x9gLOe8qa51XxvJ3vLQtmyf52CTey1Qcez3A6W6ROH5HIz5Q5bW+0VpmZb8WBohieMFGpig==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/engines": "5.19.1" + "@prisma/engines": "5.20.0" }, "bin": { "prisma": "build/index.js" diff --git a/package.json b/package.json index 6b1c1a09b..5b1dfcbc6 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "@nestjs/platform-express": "10.1.3", "@nestjs/schedule": "3.0.2", "@nestjs/serve-static": "4.0.0", - "@prisma/client": "5.19.1", + "@prisma/client": "5.20.0", "@simplewebauthn/browser": "9.0.1", "@simplewebauthn/server": "9.0.3", "@stripe/stripe-js": "3.5.0", @@ -128,7 +128,7 @@ "passport": "0.7.0", "passport-google-oauth20": "2.0.0", "passport-jwt": "4.0.1", - "prisma": "5.19.1", + "prisma": "5.20.0", "reflect-metadata": "0.1.13", "rxjs": "7.5.6", "stripe": "15.11.0",