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(); - } }