From 2f1cd29e9127e20cc9890b953cf07fbedfdfef42 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 8 Aug 2026 08:07:29 +0200 Subject: [PATCH 01/22] Task/improve language localization for DE (20260807) (#7566) * Update translations * Update changelog --- CHANGELOG.md | 1 + apps/client/src/locales/messages.de.xlf | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 509009e6f..d45f93e66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improved the check for duplicates in the preview step of the import dividends dialog (regardless of the account) - Extended the activities import to reuse an existing account of the user by name and currency - Extended the activities import to resolve an ISIN to the symbol of the data provider +- Improved the language localization for German (`de`) ### Fixed diff --git a/apps/client/src/locales/messages.de.xlf b/apps/client/src/locales/messages.de.xlf index 69463c59e..ab44d5ad9 100644 --- a/apps/client/src/locales/messages.de.xlf +++ b/apps/client/src/locales/messages.de.xlf @@ -8234,7 +8234,7 @@ Coverage - Coverage + Deckung apps/client/src/app/pages/i18n/i18n-page.html 145 @@ -8242,7 +8242,7 @@ The assets tagged as emergency fund exceed your emergency fund of ${emergencyFund} ${baseCurrency} - The assets tagged as emergency fund exceed your emergency fund of ${emergencyFund} ${baseCurrency} + Die als Notfallfonds getaggten Vermögenswerte übersteigen deinen Notfallfonds von ${emergencyFund} ${baseCurrency} apps/client/src/app/pages/i18n/i18n-page.html 147 @@ -8250,7 +8250,7 @@ Your emergency fund of ${emergencyFund} ${baseCurrency} is not covered - Your emergency fund of ${emergencyFund} ${baseCurrency} is not covered + Dein Notfallfonds von ${emergencyFund} ${baseCurrency} ist nicht gedeckt apps/client/src/app/pages/i18n/i18n-page.html 151 @@ -8258,7 +8258,7 @@ No emergency fund amount has been set to determine the coverage - No emergency fund amount has been set to determine the coverage + Es wurde kein Betrag für den Notfallfonds festgelegt, um die Deckung zu bestimmen apps/client/src/app/pages/i18n/i18n-page.html 155 @@ -8266,7 +8266,7 @@ Your emergency fund of ${emergencyFund} ${baseCurrency} is covered - Your emergency fund of ${emergencyFund} ${baseCurrency} is covered + Dein Notfallfonds von ${emergencyFund} ${baseCurrency} ist gedeckt apps/client/src/app/pages/i18n/i18n-page.html 158 @@ -8274,7 +8274,7 @@ Setup - Einrichtung + Einrichtung apps/client/src/app/pages/i18n/i18n-page.html 161 From 35582a6cd0b2985109dbec98cd2c3e7a1d4560ba Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 8 Aug 2026 08:37:01 +0200 Subject: [PATCH 02/22] Task/add missing margin on Open Startup page (#7569) --- apps/client/src/app/pages/open/open-page.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/app/pages/open/open-page.html b/apps/client/src/app/pages/open/open-page.html index 7ba6c1f4c..36641c06b 100644 --- a/apps/client/src/app/pages/open/open-page.html +++ b/apps/client/src/app/pages/open/open-page.html @@ -27,7 +27,7 @@
- +
From dbe6a3f65f19a104cb9e39817544cc6346f744b4 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 8 Aug 2026 08:58:27 +0200 Subject: [PATCH 03/22] Task/align x-axis of dividend and investment timeline charts on analysis page (#7570) * Align x-axis of dividend and investment timeline charts * Update changelog --- CHANGELOG.md | 1 + .../analysis/analysis-page.component.ts | 101 +++++++++++------- 2 files changed, 63 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d45f93e66..d2615bc00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Aligned the x-axis of the dividend and investment timeline charts on the analysis page - Improved the check for duplicates in the preview step of the activities import (regardless of the account) - Improved the check for duplicates in the preview step of the import dividends dialog (regardless of the account) - Extended the activities import to reuse an existing account of the user by name and currency diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts index 424f1bf5b..d3e3d8a31 100644 --- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts +++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts @@ -51,10 +51,11 @@ import { IonIcon } from '@ionic/angular/standalone'; import { SymbolProfile } from '@prisma/client'; import { addIcons } from 'ionicons'; import { copyOutline, ellipsisVertical } from 'ionicons/icons'; -import { isNumber, sortBy } from 'lodash'; +import { isNumber, keyBy, sortBy, union } from 'lodash'; import ms from 'ms'; import { DeviceDetectorService } from 'ngx-device-detector'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; +import { forkJoin } from 'rxjs'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, @@ -254,53 +255,75 @@ export class GfAnalysisPageComponent implements OnInit { this.isLoadingDividendTimelineChart = true; this.isLoadingInvestmentTimelineChart = true; - this.dataService - .fetchDividends({ + forkJoin({ + dividends: this.dataService.fetchDividends({ filters: this.userService.getFilters(), groupBy: this.mode(), range: this.user?.settings?.dateRange ?? DEFAULT_DATE_RANGE - }) - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(({ dividends }) => { - this.dividendsByGroup = dividends; - - this.isLoadingDividendTimelineChart = false; - - this.changeDetectorRef.markForCheck(); - }); - - this.dataService - .fetchInvestments({ + }), + investments: this.dataService.fetchInvestments({ filters: this.userService.getFilters(), groupBy: this.mode(), range: this.user?.settings?.dateRange ?? DEFAULT_DATE_RANGE }) + }) .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(({ investments, savingsRate, streaks }) => { - this.investmentsByGroup = investments; - this.savingsRatePerMonth = savingsRate; - this.streaks = streaks; - this.unitCurrentStreak = - this.mode() === 'year' - ? this.streaks?.currentStreak === 1 - ? translate('YEAR') - : translate('YEARS') - : this.streaks?.currentStreak === 1 - ? translate('MONTH') - : translate('MONTHS'); - this.unitLongestStreak = - this.mode() === 'year' - ? this.streaks?.longestStreak === 1 - ? translate('YEAR') - : translate('YEARS') - : this.streaks?.longestStreak === 1 - ? translate('MONTH') - : translate('MONTHS'); - - this.isLoadingInvestmentTimelineChart = false; + .subscribe( + ({ + dividends: { dividends }, + investments: { investments, savingsRate, streaks } + }) => { + // Expand both timelines to the union of their groups so that the + // charts share the same axis, independent of whether a dividend or + // an investment has been tracked in a given group + const dividendByDate = keyBy(dividends, 'date'); + const investmentByDate = keyBy(investments, 'date'); + + const dates = sortBy( + union(Object.keys(dividendByDate), Object.keys(investmentByDate)) + ); - this.changeDetectorRef.markForCheck(); - }); + this.dividendsByGroup = dates.map((date) => { + return { + date, + investment: dividendByDate[date]?.investment ?? 0 + }; + }); + + this.investmentsByGroup = dates.map((date) => { + return { + date, + investment: investmentByDate[date]?.investment ?? 0 + }; + }); + + this.savingsRatePerMonth = savingsRate; + this.streaks = streaks; + + this.unitCurrentStreak = + this.mode() === 'year' + ? this.streaks?.currentStreak === 1 + ? translate('YEAR') + : translate('YEARS') + : this.streaks?.currentStreak === 1 + ? translate('MONTH') + : translate('MONTHS'); + + this.unitLongestStreak = + this.mode() === 'year' + ? this.streaks?.longestStreak === 1 + ? translate('YEAR') + : translate('YEARS') + : this.streaks?.longestStreak === 1 + ? translate('MONTH') + : translate('MONTHS'); + + this.isLoadingDividendTimelineChart = false; + this.isLoadingInvestmentTimelineChart = false; + + this.changeDetectorRef.markForCheck(); + } + ); } private update() { From 6b6456339150f99a0c4de4b8ae64ae55bd7bb5a9 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 8 Aug 2026 09:25:31 +0200 Subject: [PATCH 04/22] Task/migrate account dialogs to dedicated routes (#7473) * Migrate account dialogs to dedicated routes * Update changelog --- CHANGELOG.md | 1 + .../account-detail-dialog.component.ts | 11 + .../account-dialog-host.component.ts | 296 ++++++++++++++++++ .../types/account-dialog-mode.type.ts | 1 + .../pages/accounts/accounts-page.component.ts | 199 ++---------- .../src/app/pages/accounts/accounts-page.html | 7 +- .../pages/accounts/accounts-page.routes.ts | 28 ++ libs/common/src/lib/routes/routes.ts | 21 ++ .../accounts-table.component.html | 24 +- .../accounts-table.component.stories.ts | 26 +- .../accounts-table.component.ts | 30 +- .../assistant-list-item.component.ts | 8 +- .../src/lib/assistant/assistant.component.ts | 3 +- 13 files changed, 447 insertions(+), 208 deletions(-) create mode 100644 apps/client/src/app/pages/accounts/account-dialog-host/account-dialog-host.component.ts create mode 100644 apps/client/src/app/pages/accounts/account-dialog-host/types/account-dialog-mode.type.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index d2615bc00..663fdd1fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improved the check for duplicates in the preview step of the import dividends dialog (regardless of the account) - Extended the activities import to reuse an existing account of the user by name and currency - Extended the activities import to resolve an ISIN to the symbol of the data provider +- Migrated the create, detail and edit account dialogs to dedicated routes - Improved the language localization for German (`de`) ### Fixed diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts index a0350ee6b..9aa0f6964 100644 --- a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts +++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts @@ -176,6 +176,8 @@ export class GfAccountDetailDialogComponent implements OnInit { .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { this.initialize(); + + this.refreshUser(); }); } @@ -195,6 +197,8 @@ export class GfAccountDetailDialogComponent implements OnInit { .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { this.initialize(); + + this.refreshUser(); }); } @@ -413,4 +417,11 @@ export class GfAccountDetailDialogComponent implements OnInit { this.fetchChart(); this.fetchPortfolioHoldings(); } + + private refreshUser() { + this.userService + .get(true) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(); + } } diff --git a/apps/client/src/app/pages/accounts/account-dialog-host/account-dialog-host.component.ts b/apps/client/src/app/pages/accounts/account-dialog-host/account-dialog-host.component.ts new file mode 100644 index 000000000..3734889a3 --- /dev/null +++ b/apps/client/src/app/pages/accounts/account-dialog-host/account-dialog-host.component.ts @@ -0,0 +1,296 @@ +import { GfAccountDetailDialogComponent } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.component'; +import { + AccountDetailDialogParams, + AccountDetailDialogResult +} from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces'; +import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; +import { UserService } from '@ghostfolio/client/services/user/user.service'; +import { CreateAccountDto, UpdateAccountDto } from '@ghostfolio/common/dtos'; +import { AccountResponse, User } from '@ghostfolio/common/interfaces'; +import { hasPermission, permissions } from '@ghostfolio/common/permissions'; +import { internalRoutes } from '@ghostfolio/common/routes/routes'; +import { DataService } from '@ghostfolio/ui/services'; + +import { + ChangeDetectionStrategy, + Component, + computed, + DestroyRef, + OnDestroy, + OnInit, + inject +} from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { ActivatedRoute, Router } from '@angular/router'; +import { DeviceDetectorService } from 'ngx-device-detector'; +import { Observable, of, Subject } from 'rxjs'; +import { + distinctUntilChanged, + map, + switchMap, + takeUntil, + tap +} from 'rxjs/operators'; + +import { GfCreateOrUpdateAccountDialogComponent } from '../create-or-update-account-dialog/create-or-update-account-dialog.component'; +import { CreateOrUpdateAccountDialogParams } from '../create-or-update-account-dialog/interfaces/interfaces'; +import { AccountDialogMode } from './types/account-dialog-mode.type'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'gf-account-dialog-host', + template: '' +}) +export class GfAccountDialogHostComponent implements OnDestroy, OnInit { + private dialogRef: MatDialogRef< + GfAccountDetailDialogComponent | GfCreateOrUpdateAccountDialogComponent + >; + + private readonly deviceType = computed(() => { + return this.deviceDetectorService.deviceInfo().deviceType; + }); + + private readonly dialogClosed = new Subject(); + + private readonly dataService = inject(DataService); + private readonly destroyRef = inject(DestroyRef); + private readonly deviceDetectorService = inject(DeviceDetectorService); + private readonly dialog = inject(MatDialog); + private readonly impersonationStorageService = inject( + ImpersonationStorageService + ); + private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); + private readonly userService = inject(UserService); + + public ngOnInit() { + const mode = this.route.snapshot.data.mode as AccountDialogMode; + + // The router reuses this component when only the account id changes, so + // the parameters are observed instead of read from the snapshot once + this.route.paramMap + .pipe( + map((paramMap) => { + return paramMap.get('accountId'); + }), + distinctUntilChanged(), + tap(() => { + this.closeDialog(); + }), + switchMap((accountId) => { + const account$: Observable = + mode === 'update' && accountId + ? this.dataService.fetchAccount(accountId) + : of(undefined); + + return this.userService.get().pipe( + switchMap((user) => { + return account$.pipe( + map((account) => { + return { account, accountId, user }; + }) + ); + }) + ); + }), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe({ + error: () => { + this.navigateBack(); + }, + next: ({ account, accountId, user }) => { + if (mode === 'detail') { + this.openAccountDetailDialog({ accountId, user }); + + return; + } + + if (mode === 'update') { + if ( + !account || + !hasPermission(user?.permissions, permissions.updateAccount) || + this.isReadOnlyMode(user) + ) { + this.navigateBack(); + + return; + } + + const { balance, comment, currency, id, name, platformId, tags } = + account; + + this.openCreateOrUpdateAccountDialog({ + user, + account: { + balance, + comment, + currency, + id, + name, + platformId, + tags + }, + isUpdate: true + }); + + return; + } + + if ( + !hasPermission(user?.permissions, permissions.createAccount) || + this.isReadOnlyMode(user) + ) { + this.navigateBack(); + + return; + } + + this.openCreateOrUpdateAccountDialog({ + user, + account: { + balance: 0, + comment: null, + currency: user?.settings?.baseCurrency ?? null, + id: null, + name: null, + platformId: null, + tags: [] + }, + isUpdate: false + }); + } + }); + } + + public ngOnDestroy() { + // The dialog lives in an overlay outside of this component, so it needs to + // be closed explicitly when leaving the route (for example via the browser + // navigation) + this.dialogRef?.close(); + + this.dialogClosed.complete(); + } + + private closeDialog() { + // Tear down the subscription of the dialog which is about to be replaced, + // so that its result is not mistaken for the user closing it + this.dialogClosed.next(); + + this.dialogRef?.close(); + } + + private isReadOnlyMode(user: User) { + return ( + !!this.impersonationStorageService.getId() || + !!user?.settings?.isRestrictedView + ); + } + + private navigateBack() { + void this.router.navigate(internalRoutes.accounts.routerLink); + } + + private openAccountDetailDialog({ + accountId, + user + }: { + accountId: string | null; + user: User; + }) { + if (!accountId) { + this.navigateBack(); + + return; + } + + const impersonationId = this.impersonationStorageService.getId(); + + const dialogRef = this.dialog.open< + GfAccountDetailDialogComponent, + AccountDetailDialogParams, + AccountDetailDialogResult + >(GfAccountDetailDialogComponent, { + autoFocus: false, + data: { + accountId, + impersonationId, + deviceType: this.deviceType(), + hasPermissionToCreateActivity: + !impersonationId && + hasPermission(user?.permissions, permissions.createActivity) && + !user?.settings?.isRestrictedView + }, + height: this.deviceType() === 'mobile' ? '98vh' : '80vh', + width: this.deviceType() === 'mobile' ? '100vw' : '50rem' + }); + + this.dialogRef = dialogRef; + + dialogRef + .afterClosed() + .pipe(takeUntil(this.dialogClosed), takeUntilDestroyed(this.destroyRef)) + .subscribe((result) => { + if (result?.isNavigating) { + return; + } + + this.navigateBack(); + }); + } + + private openCreateOrUpdateAccountDialog({ + account, + isUpdate, + user + }: { + account: CreateOrUpdateAccountDialogParams['account']; + isUpdate: boolean; + user: User; + }) { + const dialogRef = this.dialog.open< + GfCreateOrUpdateAccountDialogComponent, + CreateOrUpdateAccountDialogParams, + CreateAccountDto | UpdateAccountDto | null + >(GfCreateOrUpdateAccountDialogComponent, { + data: { + account, + user + }, + height: this.deviceType() === 'mobile' ? '98vh' : '80vh', + width: this.deviceType() === 'mobile' ? '100vw' : '50rem' + }); + + this.dialogRef = dialogRef; + + dialogRef + .afterClosed() + .pipe(takeUntil(this.dialogClosed), takeUntilDestroyed(this.destroyRef)) + .subscribe((result) => { + if (!result) { + this.navigateBack(); + + return; + } + + const request$: Observable = isUpdate + ? this.dataService.putAccount(result as UpdateAccountDto) + : this.dataService.postAccount(result as CreateAccountDto); + + request$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ + error: () => { + this.navigateBack(); + }, + next: () => { + // Deliberately not bound to the destroy reference: navigating back + // destroys this component and the refreshed user is what makes the + // accounts page reload its data + this.userService.get(true).subscribe(); + + this.navigateBack(); + } + }); + }); + } +} diff --git a/apps/client/src/app/pages/accounts/account-dialog-host/types/account-dialog-mode.type.ts b/apps/client/src/app/pages/accounts/account-dialog-host/types/account-dialog-mode.type.ts new file mode 100644 index 000000000..9c2d44a2a --- /dev/null +++ b/apps/client/src/app/pages/accounts/account-dialog-host/types/account-dialog-mode.type.ts @@ -0,0 +1 @@ +export type AccountDialogMode = 'create' | 'detail' | 'update'; diff --git a/apps/client/src/app/pages/accounts/accounts-page.component.ts b/apps/client/src/app/pages/accounts/accounts-page.component.ts index 9b10fb222..0f21ef55e 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.component.ts +++ b/apps/client/src/app/pages/accounts/accounts-page.component.ts @@ -1,17 +1,9 @@ -import { GfAccountDetailDialogComponent } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.component'; -import { - AccountDetailDialogParams, - AccountDetailDialogResult -} from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; -import { - CreateAccountDto, - TransferBalanceDto, - UpdateAccountDto -} from '@ghostfolio/common/dtos'; +import { TransferBalanceDto } from '@ghostfolio/common/dtos'; import { User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; +import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { AccountWithValue } from '@ghostfolio/common/types'; import { GfAccountsTableComponent } from '@ghostfolio/ui/accounts-table'; import { GfFabComponent } from '@ghostfolio/ui/fab'; @@ -30,13 +22,10 @@ import { import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; -import { Tag } from '@prisma/client'; import { DeviceDetectorService } from 'ngx-device-detector'; import { EMPTY } from 'rxjs'; import { catchError } from 'rxjs/operators'; -import { GfCreateOrUpdateAccountDialogComponent } from './create-or-update-account-dialog/create-or-update-account-dialog.component'; -import { CreateOrUpdateAccountDialogParams } from './create-or-update-account-dialog/interfaces/interfaces'; import { TransferBalanceDialogParams } from './transfer-balance/interfaces/interfaces'; import { GfTransferBalanceDialogComponent } from './transfer-balance/transfer-balance-dialog.component'; @@ -54,10 +43,13 @@ export class GfAccountsPageComponent implements OnInit { protected hasPermissionToCreateAccount: boolean; protected hasPermissionToUpdateAccount: boolean; protected impersonationId: string | null; + protected readonly internalRoutes = internalRoutes; protected totalBalanceInBaseCurrency = 0; protected totalValueInBaseCurrency = 0; protected user: User; + private isInitialFetch = true; + private readonly deviceType = computed( () => this.deviceDetectorService.deviceInfo().deviceType ); @@ -79,26 +71,7 @@ export class GfAccountsPageComponent implements OnInit { this.route.queryParams .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((params) => { - if (params['accountId'] && params['accountDetailDialog']) { - this.openAccountDetailDialog(params['accountId']); - } else if ( - params['createDialog'] && - this.hasPermissionToCreateAccount - ) { - this.openCreateAccountDialog(); - } else if (params['editDialog']) { - if (this.accounts) { - const account = this.accounts.find(({ id }) => { - return id === params['accountId']; - }); - - if (account) { - this.openUpdateAccountDialog(account); - } - } else { - this.router.navigate(['.'], { relativeTo: this.route }); - } - } else if (params['transferBalanceDialog']) { + if (params['transferBalanceDialog']) { this.openTransferBalanceDialog(); } }); @@ -130,12 +103,12 @@ export class GfAccountsPageComponent implements OnInit { this.user.permissions, permissions.updateAccount ); + + this.fetchAccounts(); } this.changeDetectorRef.markForCheck(); }); - - this.fetchAccounts(); } protected onDeleteAccount(aId: string) { @@ -149,8 +122,6 @@ export class GfAccountsPageComponent implements OnInit { .get(true) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(); - - this.fetchAccounts(); }); } @@ -160,12 +131,6 @@ export class GfAccountsPageComponent implements OnInit { }); } - protected onUpdateAccount(aAccount: AccountWithValue) { - this.router.navigate([], { - queryParams: { accountId: aAccount.id, editDialog: true } - }); - } - private fetchAccounts() { this.dataService .fetchAccounts() @@ -182,149 +147,21 @@ export class GfAccountsPageComponent implements OnInit { this.totalBalanceInBaseCurrency = totalBalanceInBaseCurrency; this.totalValueInBaseCurrency = totalValueInBaseCurrency; - if (this.accounts?.length <= 0) { - this.router.navigate([], { queryParams: { createDialog: true } }); + if ( + this.accounts?.length <= 0 && + this.hasPermissionToCreateAccount && + this.isInitialFetch + ) { + void this.router.navigate( + internalRoutes.accounts.subRoutes.create.routerLink + ); } - this.changeDetectorRef.markForCheck(); - } - ); - } - - private openUpdateAccountDialog({ - balance, - comment, - currency, - id, - name, - platformId, - tags - }: AccountWithValue & { tags?: Tag[] }) { - const dialogRef = this.dialog.open< - GfCreateOrUpdateAccountDialogComponent, - CreateOrUpdateAccountDialogParams - >(GfCreateOrUpdateAccountDialogComponent, { - data: { - account: { - balance, - comment, - currency, - id, - name, - platformId, - tags - }, - user: this.user - }, - height: this.deviceType() === 'mobile' ? '98vh' : '80vh', - width: this.deviceType() === 'mobile' ? '100vw' : '50rem' - }); - - dialogRef - .afterClosed() - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe((account: UpdateAccountDto | null) => { - if (account) { - this.reset(); - - this.dataService - .putAccount(account) - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(() => { - this.userService - .get(true) - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(); - - this.fetchAccounts(); - }); + this.isInitialFetch = false; this.changeDetectorRef.markForCheck(); } - - this.router.navigate(['.'], { relativeTo: this.route }); - }); - } - - private openAccountDetailDialog(aAccountId: string) { - const dialogRef = this.dialog.open< - GfAccountDetailDialogComponent, - AccountDetailDialogParams, - AccountDetailDialogResult - >(GfAccountDetailDialogComponent, { - autoFocus: false, - data: { - accountId: aAccountId, - deviceType: this.deviceType(), - hasPermissionToCreateActivity: - !this.hasImpersonationId && - hasPermission(this.user?.permissions, permissions.createActivity) && - !this.user?.settings?.isRestrictedView, - impersonationId: this.impersonationId - }, - height: this.deviceType() === 'mobile' ? '98vh' : '80vh', - width: this.deviceType() === 'mobile' ? '100vw' : '50rem' - }); - - dialogRef - .afterClosed() - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe((result) => { - if (result?.isNavigating) { - return; - } - - this.fetchAccounts(); - - this.router.navigate(['.'], { relativeTo: this.route }); - }); - } - - private openCreateAccountDialog() { - const dialogRef = this.dialog.open< - GfCreateOrUpdateAccountDialogComponent, - CreateOrUpdateAccountDialogParams - >(GfCreateOrUpdateAccountDialogComponent, { - data: { - account: { - balance: 0, - comment: null, - currency: this.user?.settings?.baseCurrency ?? null, - id: null, - name: null, - platformId: null, - tags: [] - }, - user: this.user - } satisfies CreateOrUpdateAccountDialogParams, - height: this.deviceType() === 'mobile' ? '98vh' : '80vh', - width: this.deviceType() === 'mobile' ? '100vw' : '50rem' - }); - - dialogRef - .afterClosed() - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe((account: CreateAccountDto | null) => { - if (account) { - this.reset(); - - this.dataService - .postAccount(account) - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(() => { - this.userService - .get(true) - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(); - - this.fetchAccounts(); - }); - - this.changeDetectorRef.markForCheck(); - } - - this.router.navigate(['.'], { relativeTo: this.route }); - }); + ); } private openTransferBalanceDialog() { diff --git a/apps/client/src/app/pages/accounts/accounts-page.html b/apps/client/src/app/pages/accounts/accounts-page.html index 1bdedbbb9..8935f70f5 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.html +++ b/apps/client/src/app/pages/accounts/accounts-page.html @@ -15,7 +15,6 @@ [totalBalanceInBaseCurrency]="totalBalanceInBaseCurrency" [totalValueInBaseCurrency]="totalValueInBaseCurrency" (accountDeleted)="onDeleteAccount($event)" - (accountToUpdate)="onUpdateAccount($event)" (transferBalance)="onTransferBalance()" />
@@ -26,6 +25,10 @@ hasPermissionToCreateAccount && !user.settings.isRestrictedView ) { - + }
+ + diff --git a/apps/client/src/app/pages/accounts/accounts-page.routes.ts b/apps/client/src/app/pages/accounts/accounts-page.routes.ts index e4edc39c1..fd58d660c 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.routes.ts +++ b/apps/client/src/app/pages/accounts/accounts-page.routes.ts @@ -3,11 +3,39 @@ import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { Routes } from '@angular/router'; +import { GfAccountDialogHostComponent } from './account-dialog-host/account-dialog-host.component'; import { GfAccountsPageComponent } from './accounts-page.component'; +const { create, detail, update } = internalRoutes.accounts.subRoutes; + export const routes: Routes = [ { canActivate: [AuthGuard], + children: [ + { + component: GfAccountDialogHostComponent, + data: { mode: 'create' }, + path: create.path, + title: create.title + }, + { + children: [ + { + component: GfAccountDialogHostComponent, + data: { mode: 'detail' }, + path: '', + title: detail.title + }, + { + component: GfAccountDialogHostComponent, + data: { mode: 'update' }, + path: update.path, + title: update.title + } + ], + path: ':accountId' + } + ], component: GfAccountsPageComponent, path: '', title: internalRoutes.accounts.title diff --git a/libs/common/src/lib/routes/routes.ts b/libs/common/src/lib/routes/routes.ts index 4d40cc5fb..d1cc1d430 100644 --- a/libs/common/src/lib/routes/routes.ts +++ b/libs/common/src/lib/routes/routes.ts @@ -66,6 +66,27 @@ export const internalRoutes = { accounts: { path: 'accounts', routerLink: ['/accounts'], + subRoutes: { + create: { + path: 'create', + routerLink: ['/accounts', 'create'], + title: $localize`Add Account` + }, + detail: { + path: undefined, // Default sub route + routerLink: (aAccountId: string) => { + return ['/accounts', aAccountId]; + }, + title: $localize`Account` + }, + update: { + path: 'update', + routerLink: (aAccountId: string) => { + return ['/accounts', aAccountId, 'update']; + }, + title: $localize`Update Account` + } + }, title: $localize`Accounts` }, api: { diff --git a/libs/ui/src/lib/accounts-table/accounts-table.component.html b/libs/ui/src/lib/accounts-table/accounts-table.component.html index 1d1cbb296..b6e0d846c 100644 --- a/libs/ui/src/lib/accounts-table/accounts-table.component.html +++ b/libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -307,18 +307,26 @@ - - +
-@if (this.showNextArrow) { +@if (showNextArrow) { - @for (accessItem of user()?.access; track accessItem) { + @for (accessItem of user()?.access; track accessItem.id) {