diff --git a/CHANGELOG.md b/CHANGELOG.md
index 855def05a..04d33dca3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Restricted the _Restricted view and manage_ permission of the access to share the portfolio to the Model Context Protocol (MCP) (experimental)
+- Migrated the transfer cash balance dialog to a dedicated route
- Improved the language localization for Italian (`it`)
- Upgraded `prisma` from version `7.9.1` to `7.10.0`
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
index 29f7eb78b..0cc9476e3 100644
--- 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
@@ -5,11 +5,17 @@ import {
} 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 {
+ CreateAccountDto,
+ TransferBalanceDto,
+ 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 { Scope, hasScope, scopes } from '@ghostfolio/common/scopes';
+import { AccountWithPlatform } from '@ghostfolio/common/types';
+import { NotificationService } from '@ghostfolio/ui/notifications';
import { DataService } from '@ghostfolio/ui/services';
import {
@@ -36,6 +42,8 @@ import {
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';
import { AccountDialogMode } from './types/account-dialog-mode.type';
@Component({
@@ -45,7 +53,9 @@ import { AccountDialogMode } from './types/account-dialog-mode.type';
})
export class GfAccountDialogHostComponent implements OnDestroy, OnInit {
private dialogRef: MatDialogRef<
- GfAccountDetailDialogComponent | GfCreateOrUpdateAccountDialogComponent
+ | GfAccountDetailDialogComponent
+ | GfCreateOrUpdateAccountDialogComponent
+ | GfTransferBalanceDialogComponent
>;
private readonly deviceType = computed(() => {
@@ -61,6 +71,7 @@ export class GfAccountDialogHostComponent implements OnDestroy, OnInit {
private readonly impersonationStorageService = inject(
ImpersonationStorageService
);
+ private readonly notificationService = inject(NotificationService);
private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router);
private readonly userService = inject(UserService);
@@ -108,6 +119,24 @@ export class GfAccountDialogHostComponent implements OnDestroy, OnInit {
return;
}
+ if (mode === 'transferCashBalance') {
+ const accounts = user?.accounts ?? [];
+
+ if (
+ accounts.length < 2 ||
+ !hasPermission(user?.permissions, permissions.updateAccount) ||
+ this.isWriteRestricted(user, scopes.accountUpdate)
+ ) {
+ this.navigateBack();
+
+ return;
+ }
+
+ this.openTransferBalanceDialog({ accounts });
+
+ return;
+ }
+
if (mode === 'update') {
if (
!account ||
@@ -298,4 +327,55 @@ export class GfAccountDialogHostComponent implements OnDestroy, OnInit {
});
});
}
+
+ private openTransferBalanceDialog({
+ accounts
+ }: {
+ accounts: AccountWithPlatform[];
+ }) {
+ const dialogRef = this.dialog.open<
+ GfTransferBalanceDialogComponent,
+ TransferBalanceDialogParams,
+ TransferBalanceDto | null
+ >(GfTransferBalanceDialogComponent, {
+ data: {
+ accounts
+ },
+ 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;
+ }
+
+ this.dataService
+ .transferAccountBalance(result)
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe({
+ error: () => {
+ this.notificationService.alert({
+ title: $localize`Oops, cash balance transfer has failed.`
+ });
+
+ 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
index 9c2d44a2a..4cdf112a7 100644
--- 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
@@ -1 +1,2 @@
-export type AccountDialogMode = 'create' | 'detail' | 'update';
+export type AccountDialogMode =
+ 'create' | 'detail' | 'transferCashBalance' | '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 2389cbaa8..f731962a5 100644
--- a/apps/client/src/app/pages/accounts/accounts-page.component.ts
+++ b/apps/client/src/app/pages/accounts/accounts-page.component.ts
@@ -1,7 +1,6 @@
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { DEFAULT_LOCALE } from '@ghostfolio/common/config';
-import { TransferBalanceDto } from '@ghostfolio/common/dtos';
import { AccountsResponse, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { internalRoutes } from '@ghostfolio/common/routes/routes';
@@ -9,27 +8,19 @@ import { hasScope, scopes } from '@ghostfolio/common/scopes';
import { AccountWithValue } from '@ghostfolio/common/types';
import { GfAccountsTableComponent } from '@ghostfolio/ui/accounts-table';
import { GfFabComponent } from '@ghostfolio/ui/fab';
-import { NotificationService } from '@ghostfolio/ui/notifications';
import { DataService } from '@ghostfolio/ui/services';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
- computed,
DestroyRef,
inject,
OnInit
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
-import { MatDialog } from '@angular/material/dialog';
-import { ActivatedRoute, Router, RouterModule } from '@angular/router';
-import { DeviceDetectorService } from 'ngx-device-detector';
-import { filter, of, switchMap, tap } from 'rxjs';
-import { catchError } from 'rxjs/operators';
-
-import { TransferBalanceDialogParams } from './transfer-balance/interfaces/interfaces';
-import { GfTransferBalanceDialogComponent } from './transfer-balance/transfer-balance-dialog.component';
+import { Router, RouterModule } from '@angular/router';
+import { filter, switchMap, tap } from 'rxjs';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -54,33 +45,15 @@ export class GfAccountsPageComponent implements OnInit {
private hasImpersonationId: boolean;
- private readonly deviceType = computed(
- () => this.deviceDetectorService.deviceInfo().deviceType
- );
-
private readonly changeDetectorRef = inject(ChangeDetectorRef);
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 notificationService = inject(NotificationService);
- private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router);
private readonly userService = inject(UserService);
- public constructor() {
- this.route.queryParams
- .pipe(takeUntilDestroyed(this.destroyRef))
- .subscribe((params) => {
- if (params['transferBalanceDialog']) {
- this.openTransferBalanceDialog();
- }
- });
- }
-
public ngOnInit() {
this.impersonationStorageService
.onChangeHasImpersonation()
@@ -135,67 +108,12 @@ export class GfAccountsPageComponent implements OnInit {
});
}
- protected onTransferBalance() {
- this.router.navigate([], {
- queryParams: { transferBalanceDialog: true }
- });
- }
-
private fetchAccounts() {
return this.dataService.fetchAccounts({
filters: this.userService.getFilters()
});
}
- private openTransferBalanceDialog() {
- const dialogRef = this.dialog.open<
- GfTransferBalanceDialogComponent,
- TransferBalanceDialogParams
- >(GfTransferBalanceDialogComponent, {
- data: {
- accounts: this.user?.accounts
- },
- width: this.deviceType() === 'mobile' ? '100vw' : '50rem'
- });
-
- dialogRef
- .afterClosed()
- .pipe(takeUntilDestroyed(this.destroyRef))
- .subscribe((data: any) => {
- if (data) {
- this.reset();
-
- const { accountIdFrom, accountIdTo, balance }: TransferBalanceDto =
- data?.account;
-
- this.dataService
- .transferAccountBalance({
- accountIdFrom,
- accountIdTo,
- balance
- })
- .pipe(
- catchError(() => {
- this.notificationService.alert({
- title: $localize`Oops, cash balance transfer has failed.`
- });
-
- return of(undefined);
- }),
- switchMap(() => this.fetchAccounts()),
- takeUntilDestroyed(this.destroyRef)
- )
- .subscribe((response) => {
- this.updateAccounts(response);
- });
-
- this.changeDetectorRef.markForCheck();
- }
-
- this.router.navigate(['.'], { relativeTo: this.route });
- });
- }
-
private reset() {
this.accounts = undefined;
this.activitiesCount = 0;
diff --git a/apps/client/src/app/pages/accounts/accounts-page.html b/apps/client/src/app/pages/accounts/accounts-page.html
index 55a8f6534..a9a344ce7 100644
--- a/apps/client/src/app/pages/accounts/accounts-page.html
+++ b/apps/client/src/app/pages/accounts/accounts-page.html
@@ -16,7 +16,6 @@
[totalBalanceInBaseCurrency]="totalBalanceInBaseCurrency"
[totalValueInBaseCurrency]="totalValueInBaseCurrency"
(accountDeleted)="onDeleteAccount($event)"
- (transferBalance)="onTransferBalance()"
/>
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 fd58d660c..74ebb3a77 100644
--- a/apps/client/src/app/pages/accounts/accounts-page.routes.ts
+++ b/apps/client/src/app/pages/accounts/accounts-page.routes.ts
@@ -6,7 +6,8 @@ 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;
+const { create, detail, transferCashBalance, update } =
+ internalRoutes.accounts.subRoutes;
export const routes: Routes = [
{
@@ -18,6 +19,12 @@ export const routes: Routes = [
path: create.path,
title: create.title
},
+ {
+ component: GfAccountDialogHostComponent,
+ data: { mode: 'transferCashBalance' },
+ path: transferCashBalance.path,
+ title: transferCashBalance.title
+ },
{
children: [
{
diff --git a/apps/client/src/app/pages/accounts/transfer-balance/transfer-balance-dialog.component.ts b/apps/client/src/app/pages/accounts/transfer-balance/transfer-balance-dialog.component.ts
index 75f6f076a..cdfa7181c 100644
--- a/apps/client/src/app/pages/accounts/transfer-balance/transfer-balance-dialog.component.ts
+++ b/apps/client/src/app/pages/accounts/transfer-balance/transfer-balance-dialog.component.ts
@@ -99,13 +99,13 @@ export class GfTransferBalanceDialogComponent {
}
protected onSubmit() {
- const account: TransferBalanceDto = {
+ const transferBalance: TransferBalanceDto = {
accountIdFrom: this.transferBalanceForm.controls.fromAccount.value ?? '',
accountIdTo: this.transferBalanceForm.controls.toAccount.value ?? '',
balance: Number(this.transferBalanceForm.controls.balance.value)
};
- this.dialogRef.close({ account });
+ this.dialogRef.close(transferBalance);
}
private getAccountById(aId: string | null) {
diff --git a/libs/common/src/lib/routes/routes.ts b/libs/common/src/lib/routes/routes.ts
index 6985ba04f..3d7429760 100644
--- a/libs/common/src/lib/routes/routes.ts
+++ b/libs/common/src/lib/routes/routes.ts
@@ -93,6 +93,11 @@ export const internalRoutes = {
},
title: $localize`Account`
},
+ transferCashBalance: {
+ path: 'transfer-cash-balance',
+ routerLink: ['/accounts', 'transfer-cash-balance'],
+ title: $localize`Transfer Cash Balance`
+ },
update: {
path: 'update',
routerLink: (aAccountId: string) => {
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 2719f05a0..a01abff1a 100644
--- a/libs/ui/src/lib/accounts-table/accounts-table.component.html
+++ b/libs/ui/src/lib/accounts-table/accounts-table.component.html
@@ -4,7 +4,7 @@
class="align-items-center d-flex"
mat-stroked-button
[disabled]="dataSource?.data.length < 2"
- (click)="onTransferBalance()"
+ [routerLink]="transferCashBalanceRouterLink"
>
Transfer Cash Balance...
diff --git a/libs/ui/src/lib/accounts-table/accounts-table.component.ts b/libs/ui/src/lib/accounts-table/accounts-table.component.ts
index 48eb99d51..e98693290 100644
--- a/libs/ui/src/lib/accounts-table/accounts-table.component.ts
+++ b/libs/ui/src/lib/accounts-table/accounts-table.component.ts
@@ -75,7 +75,6 @@ export class GfAccountsTableComponent {
public readonly totalValueInBaseCurrency = input();
public readonly accountDeleted = output();
- public readonly transferBalance = output();
public readonly sort = viewChild.required(MatSort);
@@ -141,6 +140,9 @@ export class GfAccountsTableComponent {
protected readonly isLoading = computed(() => !this.accounts());
+ protected readonly transferCashBalanceRouterLink =
+ internalRoutes.accounts.subRoutes.transferCashBalance.routerLink;
+
private readonly notificationService = inject(NotificationService);
private readonly router = inject(Router);
@@ -197,8 +199,4 @@ export class GfAccountsTableComponent {
title: aComment
});
}
-
- protected onTransferBalance() {
- this.transferBalance.emit();
- }
}