Add support to delete a cash balance (#2707)
This commit is contained in:
parent
77b13b88f0
commit
377ba75e4c
@ -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,6 +123,69 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
||||
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(
|
||||
this.activities.map(({ id }) => {
|
||||
return id;
|
||||
})
|
||||
)
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((data) => {
|
||||
downloadAsFile({
|
||||
content: data,
|
||||
fileName: `ghostfolio-export-${this.name
|
||||
.replace(/\s+/g, '-')
|
||||
.toLowerCase()}-${format(
|
||||
parseISO(data.meta.date),
|
||||
'yyyyMMddHHmm'
|
||||
)}.json`,
|
||||
format: 'json'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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: [
|
||||
@ -141,39 +215,6 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.impersonationStorageService
|
||||
.onChangeHasImpersonation()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((impersonationId) => {
|
||||
this.hasImpersonationId = !!impersonationId;
|
||||
});
|
||||
}
|
||||
|
||||
public onClose() {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
public onExport() {
|
||||
this.dataService
|
||||
.fetchExport(
|
||||
this.activities.map(({ id }) => {
|
||||
return id;
|
||||
})
|
||||
)
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((data) => {
|
||||
downloadAsFile({
|
||||
content: data,
|
||||
fileName: `ghostfolio-export-${this.name
|
||||
.replace(/\s+/g, '-')
|
||||
.toLowerCase()}-${format(
|
||||
parseISO(data.meta.date),
|
||||
'yyyyMMddHHmm'
|
||||
)}.json`,
|
||||
format: 'json'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
|
@ -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 ngOnChanges() {
|
||||
if (this.accountBalances) {
|
||||
this.dataSource = new MatTableDataSource(this.accountBalances);
|
||||
|
||||
this.dataSource.sort = this.sort;
|
||||
this.dataSource.sortingDataAccessor = get;
|
||||
}
|
||||
}
|
||||
|
||||
public onDeleteAccountBalance(aId: string) {
|
||||
const confirmation = confirm(
|
||||
$localize`Do you really want to delete this account balance?`
|
||||
);
|
||||
|
||||
if (confirmation) {
|
||||
this.accountBalanceDeleted.emit(aId);
|
||||
}
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
private fetchBalances() {
|
||||
this.dataService
|
||||
.fetchAccountBalances(this.accountId)
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ balances }) => {
|
||||
this.dataSource = new MatTableDataSource(balances);
|
||||
|
||||
this.dataSource.sort = this.sort;
|
||||
this.dataSource.sortingDataAccessor = get;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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…
x
Reference in New Issue
Block a user