import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; 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 { 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 { EMPTY } from 'rxjs'; import { catchError } from 'rxjs/operators'; import { TransferBalanceDialogParams } from './transfer-balance/interfaces/interfaces'; import { GfTransferBalanceDialogComponent } from './transfer-balance/transfer-balance-dialog.component'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'page' }, imports: [GfAccountsTableComponent, GfFabComponent, RouterModule], selector: 'gf-accounts-page', styleUrls: ['./accounts-page.scss'], templateUrl: './accounts-page.html' }) export class GfAccountsPageComponent implements OnInit { protected accounts: AccountWithValue[]; protected activitiesCount = 0; protected hasPermissionToCreateAccount: boolean; protected hasPermissionToDeleteAccount: boolean; protected hasPermissionToUpdateAccount: boolean; protected readonly internalRoutes = internalRoutes; protected totalBalanceInBaseCurrency = 0; protected totalValueInBaseCurrency = 0; protected user: User; private hasImpersonationId: boolean; private isInitialFetch = true; 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() .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((impersonationId) => { this.hasImpersonationId = !!impersonationId; }); this.userService.stateChanged .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((state) => { if (state?.user) { this.user = state.user; this.hasPermissionToCreateAccount = hasPermission(this.user.permissions, permissions.createAccount) && hasScope(this.user.scopes, scopes.accountCreate); this.hasPermissionToDeleteAccount = hasPermission(this.user.permissions, permissions.deleteAccount) && hasScope(this.user.scopes, scopes.accountDelete); this.hasPermissionToUpdateAccount = hasPermission(this.user.permissions, permissions.updateAccount) && hasScope(this.user.scopes, scopes.accountUpdate); this.fetchAccounts(); } this.changeDetectorRef.markForCheck(); }); } protected onDeleteAccount(aId: string) { this.reset(); this.dataService .deleteAccount(aId) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { this.userService .get(true) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(); }); } protected onTransferBalance() { this.router.navigate([], { queryParams: { transferBalanceDialog: true } }); } private fetchAccounts() { this.dataService .fetchAccounts() .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe( ({ accounts, activitiesCount, totalBalanceInBaseCurrency, totalValueInBaseCurrency }) => { this.accounts = accounts; this.activitiesCount = activitiesCount; this.totalBalanceInBaseCurrency = totalBalanceInBaseCurrency; this.totalValueInBaseCurrency = totalValueInBaseCurrency; if ( !this.hasImpersonationId && this.accounts?.length <= 0 && this.hasPermissionToCreateAccount && this.isInitialFetch ) { void this.router.navigate( internalRoutes.accounts.subRoutes.create.routerLink ); } this.isInitialFetch = false; this.changeDetectorRef.markForCheck(); } ); } private openTransferBalanceDialog() { const dialogRef = this.dialog.open< GfTransferBalanceDialogComponent, TransferBalanceDialogParams >(GfTransferBalanceDialogComponent, { data: { accounts: this.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 EMPTY; }), takeUntilDestroyed(this.destroyRef) ) .subscribe(() => { this.fetchAccounts(); }); this.changeDetectorRef.markForCheck(); } this.router.navigate(['.'], { relativeTo: this.route }); }); } private reset() { this.accounts = []; this.activitiesCount = 0; this.totalBalanceInBaseCurrency = 0; this.totalValueInBaseCurrency = 0; } }