Add support to delete a cash balance (#2707)

pull/2709/head
Thomas Kaul 10 months ago committed by GitHub
parent 77b13b88f0
commit 377ba75e4c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,51 @@
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
Controller,
Delete,
HttpException,
Inject,
Param,
UseGuards
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AccountBalanceService } from './account-balance.service';
import { AuthGuard } from '@nestjs/passport';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { AccountBalance } from '@prisma/client';
@Controller('account-balance')
export class AccountBalanceController {
public constructor(
private readonly accountBalanceService: AccountBalanceService,
@Inject(REQUEST) private readonly request: RequestWithUser
) {}
@Delete(':id')
@UseGuards(AuthGuard('jwt'))
public async deleteAccountBalance(
@Param('id') id: string
): Promise<AccountBalance> {
const accountBalance = await this.accountBalanceService.accountBalance({
id
});
if (
!hasPermission(
this.request.user.permissions,
permissions.deleteAccountBalance
) ||
!accountBalance ||
accountBalance.userId !== this.request.user.id
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.accountBalanceService.deleteAccountBalance({
id
});
}
}

@ -1,9 +1,12 @@
import { AccountBalanceService } from '@ghostfolio/api/services/account-balance/account-balance.service';
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { Module } from '@nestjs/common';
import { AccountBalanceController } from './account-balance.controller';
import { AccountBalanceService } from './account-balance.service';
@Module({
controllers: [AccountBalanceController],
exports: [AccountBalanceService],
imports: [ExchangeRateDataModule, PrismaModule],
providers: [AccountBalanceService]

@ -12,6 +12,17 @@ export class AccountBalanceService {
private readonly prismaService: PrismaService
) {}
public async accountBalance(
accountBalanceWhereInput: Prisma.AccountBalanceWhereInput
): Promise<AccountBalance | null> {
return this.prismaService.accountBalance.findFirst({
include: {
Account: true
},
where: accountBalanceWhereInput
});
}
public async createAccountBalance(
data: Prisma.AccountBalanceCreateInput
): Promise<AccountBalance> {
@ -20,6 +31,14 @@ export class AccountBalanceService {
});
}
public async deleteAccountBalance(
where: Prisma.AccountBalanceWhereUniqueInput
): Promise<AccountBalance> {
return this.prismaService.accountBalance.delete({
where
});
}
public async getAccountBalances({
filters,
user,

@ -1,6 +1,6 @@
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor';
import { AccountBalanceService } from '@ghostfolio/api/services/account-balance/account-balance.service';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
import {

@ -1,7 +1,7 @@
import { AccountBalanceModule } from '@ghostfolio/api/app/account-balance/account-balance.module';
import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { AccountBalanceModule } from '@ghostfolio/api/services/account-balance/account-balance.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';

@ -1,4 +1,4 @@
import { AccountBalanceService } from '@ghostfolio/api/services/account-balance/account-balance.service';
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { Filter } from '@ghostfolio/common/interfaces';

@ -1,8 +1,8 @@
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { CacheModule } from '@ghostfolio/api/app/cache/cache.module';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { AccountBalanceService } from '@ghostfolio/api/services/account-balance/account-balance.service';
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';

@ -1,8 +1,8 @@
import { AccessModule } from '@ghostfolio/api/app/access/access.module';
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { AccountBalanceService } from '@ghostfolio/api/services/account-balance/account-balance.service';
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';

@ -1,3 +1,4 @@
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details.interface';
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
@ -12,7 +13,6 @@ import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from '@ghostfolio/ap
import { CurrencyClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/current-investment';
import { EmergencyFundSetup } from '@ghostfolio/api/models/rules/emergency-fund/emergency-fund-setup';
import { FeeRatioInitialInvestment } from '@ghostfolio/api/models/rules/fees/fee-ratio-initial-investment';
import { AccountBalanceService } from '@ghostfolio/api/services/account-balance/account-balance.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';

@ -11,7 +11,11 @@ import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { downloadAsFile } from '@ghostfolio/common/helper';
import { HistoricalDataItem, User } from '@ghostfolio/common/interfaces';
import {
AccountBalancesResponse,
HistoricalDataItem,
User
} from '@ghostfolio/common/interfaces';
import { OrderWithAccount } from '@ghostfolio/common/types';
import Big from 'big.js';
import { format, parseISO } from 'date-fns';
@ -20,6 +24,7 @@ import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { AccountDetailDialogParams } from './interfaces/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
@Component({
host: { class: 'd-flex flex-column h-100' },
@ -29,11 +34,13 @@ import { AccountDetailDialogParams } from './interfaces/interfaces';
styleUrls: ['./account-detail-dialog.component.scss']
})
export class AccountDetailDialog implements OnDestroy, OnInit {
public accountBalances: AccountBalancesResponse['balances'];
public activities: OrderWithAccount[];
public balance: number;
public currency: string;
public equity: number;
public hasImpersonationId: boolean;
public hasPermissionToDeleteAccountBalance: boolean;
public historicalDataItems: HistoricalDataItem[];
public isLoadingActivities: boolean;
public isLoadingChart: boolean;
@ -59,6 +66,11 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
if (state?.user) {
this.user = state.user;
this.hasPermissionToDeleteAccountBalance = hasPermission(
this.user.permissions,
permissions.deleteAccountBalance
);
this.changeDetectorRef.markForCheck();
}
});
@ -66,7 +78,6 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
public ngOnInit() {
this.isLoadingActivities = true;
this.isLoadingChart = true;
this.dataService
.fetchAccount(this.data.accountId)
@ -112,48 +123,33 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
this.changeDetectorRef.markForCheck();
});
this.dataService
.fetchPortfolioPerformance({
filters: [
{
id: this.data.accountId,
type: 'ACCOUNT'
}
],
range: 'max',
withExcludedAccounts: true
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ chart }) => {
this.historicalDataItems = chart.map(
({ date, netWorth, netWorthInPercentage }) => {
return {
date,
value:
this.hasImpersonationId || this.user.settings.isRestrictedView
? netWorthInPercentage
: netWorth
};
}
);
this.isLoadingChart = false;
this.changeDetectorRef.markForCheck();
});
this.impersonationStorageService
.onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId;
});
this.fetchAccountBalances();
this.fetchPortfolioPerformance();
}
public onClose() {
this.dialogRef.close();
}
public onDeleteAccountBalance(aId: string) {
this.dataService
.deleteAccountBalance(aId)
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe({
next: () => {
this.fetchAccountBalances();
this.fetchPortfolioPerformance();
}
});
}
public onExport() {
this.dataService
.fetchExport(
@ -176,6 +172,51 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
});
}
private fetchAccountBalances() {
this.dataService
.fetchAccountBalances(this.data.accountId)
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ balances }) => {
this.accountBalances = balances;
this.changeDetectorRef.markForCheck();
});
}
private fetchPortfolioPerformance() {
this.isLoadingChart = true;
this.dataService
.fetchPortfolioPerformance({
filters: [
{
id: this.data.accountId,
type: 'ACCOUNT'
}
],
range: 'max',
withExcludedAccounts: true
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ chart }) => {
this.historicalDataItems = chart.map(
({ date, netWorth, netWorthInPercentage }) => {
return {
date,
value:
this.hasImpersonationId || this.user.settings.isRestrictedView
? netWorthInPercentage
: netWorth
};
}
);
this.isLoadingChart = false;
this.changeDetectorRef.markForCheck();
});
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();

@ -87,8 +87,11 @@
<mat-tab>
<ng-template i18n mat-tab-label>Cash Balances</ng-template>
<gf-account-balances
[accountBalances]="accountBalances"
[accountId]="data.accountId"
[locale]="user?.settings?.locale"
[showActions]="!hasImpersonationId && hasPermissionToDeleteAccountBalance && !user.settings.isRestrictedView"
(accountBalanceDeleted)="onDeleteAccountBalance($event)"
></gf-account-balances>
</mat-tab>
</mat-tab-group>

@ -212,6 +212,10 @@ export class DataService {
return this.http.delete<any>(`/api/v1/account/${aId}`);
}
public deleteAccountBalance(aId: string) {
return this.http.delete<any>(`/api/v1/account-balance/${aId}`);
}
public deleteAllOrders() {
return this.http.delete<any>(`/api/v1/order/`);
}

@ -12,6 +12,7 @@ export const permissions = {
createUserAccount: 'createUserAccount',
deleteAccess: 'deleteAccess',
deleteAccount: 'deleteAcccount',
deleteAccountBalance: 'deleteAcccountBalance',
deleteAuthDevice: 'deleteAuthDevice',
deleteOrder: 'deleteOrder',
deletePlatform: 'deletePlatform',
@ -45,6 +46,7 @@ export function getPermissions(aRole: Role): string[] {
permissions.accessAssistant,
permissions.createAccess,
permissions.createAccount,
permissions.deleteAccountBalance,
permissions.createOrder,
permissions.createPlatform,
permissions.createTag,
@ -75,6 +77,7 @@ export function getPermissions(aRole: Role): string[] {
permissions.createOrder,
permissions.deleteAccess,
permissions.deleteAccount,
permissions.deleteAccountBalance,
permissions.deleteAuthDevice,
permissions.deleteOrder,
permissions.updateAccount,

@ -31,6 +31,29 @@
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th *matHeaderCellDef class="px-1 text-center" mat-header-cell></th>
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
<button
*ngIf="showActions"
class="mx-1 no-min-width px-2"
mat-button
[matMenuTriggerFor]="accountBalanceMenu"
(click)="$event.stopPropagation()"
>
<ion-icon name="ellipsis-horizontal"></ion-icon>
</button>
<mat-menu #accountBalanceMenu="matMenu" xPosition="before">
<button mat-menu-item (click)="onDeleteAccountBalance(element.id)">
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="trash-outline"></ion-icon>
<span i18n>Delete</span>
</span>
</button>
</mat-menu>
</td>
</ng-container>
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
</table>

@ -1,18 +1,19 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
EventEmitter,
Input,
OnChanges,
OnDestroy,
OnInit,
Output,
ViewChild
} from '@angular/core';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { DataService } from '@ghostfolio/client/services/data.service';
import { AccountBalancesResponse } from '@ghostfolio/common/interfaces';
import { get } from 'lodash';
import { Subject, takeUntil } from 'rxjs';
import { Subject } from 'rxjs';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
@ -20,44 +21,48 @@ import { Subject, takeUntil } from 'rxjs';
styleUrls: ['./account-balances.component.scss'],
templateUrl: './account-balances.component.html'
})
export class AccountBalancesComponent implements OnDestroy, OnInit {
export class AccountBalancesComponent implements OnChanges, OnDestroy, OnInit {
@Input() accountBalances: AccountBalancesResponse['balances'];
@Input() accountId: string;
@Input() locale: string;
@Input() showActions = true;
@Output() accountBalanceDeleted = new EventEmitter<string>();
@ViewChild(MatSort) sort: MatSort;
public dataSource: MatTableDataSource<
AccountBalancesResponse['balances'][0]
> = new MatTableDataSource();
public displayedColumns: string[] = ['date', 'value'];
public displayedColumns: string[] = ['date', 'value', 'actions'];
private unsubscribeSubject = new Subject<void>();
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService
) {}
public constructor() {}
public ngOnInit() {
this.fetchBalances();
}
public ngOnInit() {}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
public ngOnChanges() {
if (this.accountBalances) {
this.dataSource = new MatTableDataSource(this.accountBalances);
this.dataSource.sort = this.sort;
this.dataSource.sortingDataAccessor = get;
}
}
private fetchBalances() {
this.dataService
.fetchAccountBalances(this.accountId)
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ balances }) => {
this.dataSource = new MatTableDataSource(balances);
public onDeleteAccountBalance(aId: string) {
const confirmation = confirm(
$localize`Do you really want to delete this account balance?`
);
this.dataSource.sort = this.sort;
this.dataSource.sortingDataAccessor = get;
if (confirmation) {
this.accountBalanceDeleted.emit(aId);
}
}
this.changeDetectorRef.markForCheck();
});
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

@ -1,5 +1,7 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatMenuModule } from '@angular/material/menu';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { GfValueModule } from '@ghostfolio/ui/value';
@ -9,7 +11,14 @@ import { AccountBalancesComponent } from './account-balances.component';
@NgModule({
declarations: [AccountBalancesComponent],
exports: [AccountBalancesComponent],
imports: [CommonModule, GfValueModule, MatSortModule, MatTableModule],
imports: [
CommonModule,
GfValueModule,
MatButtonModule,
MatMenuModule,
MatSortModule,
MatTableModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfAccountBalancesModule {}

Loading…
Cancel
Save