From dfacbed66d7b72b9453b53bf8564a6d0c8695cdf Mon Sep 17 00:00:00 2001 From: Bastien Jeannelle <48835068+Sonlis@users.noreply.github.com> Date: Sat, 20 Apr 2024 12:00:00 +0300 Subject: [PATCH] Feature/add support to create account cash balances (#3260) * Add support to create account cash balances * Update changelog --------- Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> --- CHANGELOG.md | 1 + .../account-balance.controller.ts | 39 +++++ .../account-balance/account-balance.module.ts | 3 +- .../create-account-balance.dto.ts | 12 ++ .../account-detail-dialog.component.ts | 28 +++- .../account-detail-dialog.html | 2 + apps/client/src/app/services/data.service.ts | 22 ++- libs/common/src/lib/permissions.ts | 3 + .../account-balances.component.html | 152 ++++++++++++------ .../account-balances.component.scss | 7 + .../account-balances.component.ts | 38 ++++- 11 files changed, 244 insertions(+), 63 deletions(-) create mode 100644 apps/api/src/app/account-balance/create-account-balance.dto.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c88ee481d..06c7b37a9 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 ### Added - Added the date range support to the portfolio holdings page +- Added support to create an account balance ### Changed diff --git a/apps/api/src/app/account-balance/account-balance.controller.ts b/apps/api/src/app/account-balance/account-balance.controller.ts index 943d0aeb5..12f21753b 100644 --- a/apps/api/src/app/account-balance/account-balance.controller.ts +++ b/apps/api/src/app/account-balance/account-balance.controller.ts @@ -1,3 +1,4 @@ +import { AccountService } from '@ghostfolio/api/app/account/account.service'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { permissions } from '@ghostfolio/common/permissions'; @@ -5,6 +6,8 @@ import type { RequestWithUser } from '@ghostfolio/common/types'; import { Controller, + Body, + Post, Delete, HttpException, Inject, @@ -17,14 +20,50 @@ import { AccountBalance } from '@prisma/client'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { AccountBalanceService } from './account-balance.service'; +import { CreateAccountBalanceDto } from './create-account-balance.dto'; @Controller('account-balance') export class AccountBalanceController { public constructor( private readonly accountBalanceService: AccountBalanceService, + private readonly accountService: AccountService, @Inject(REQUEST) private readonly request: RequestWithUser ) {} + @HasPermission(permissions.createAccountBalance) + @Post() + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) + public async createAccountBalance( + @Body() data: CreateAccountBalanceDto + ): Promise { + const account = await this.accountService.account({ + id_userId: { + id: data.accountId, + userId: this.request.user.id + } + }); + + if (!account) { + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); + } + + return this.accountBalanceService.createAccountBalance({ + Account: { + connect: { + id_userId: { + id: account.id, + userId: account.userId + } + } + }, + date: data.date, + value: data.balance + }); + } + @HasPermission(permissions.deleteAccountBalance) @Delete(':id') @UseGuards(AuthGuard('jwt'), HasPermissionGuard) diff --git a/apps/api/src/app/account-balance/account-balance.module.ts b/apps/api/src/app/account-balance/account-balance.module.ts index 1fba60fce..02323acc9 100644 --- a/apps/api/src/app/account-balance/account-balance.module.ts +++ b/apps/api/src/app/account-balance/account-balance.module.ts @@ -1,3 +1,4 @@ +import { AccountService } from '@ghostfolio/api/app/account/account.service'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; @@ -10,6 +11,6 @@ import { AccountBalanceService } from './account-balance.service'; controllers: [AccountBalanceController], exports: [AccountBalanceService], imports: [ExchangeRateDataModule, PrismaModule], - providers: [AccountBalanceService] + providers: [AccountBalanceService, AccountService] }) export class AccountBalanceModule {} diff --git a/apps/api/src/app/account-balance/create-account-balance.dto.ts b/apps/api/src/app/account-balance/create-account-balance.dto.ts new file mode 100644 index 000000000..28e939b82 --- /dev/null +++ b/apps/api/src/app/account-balance/create-account-balance.dto.ts @@ -0,0 +1,12 @@ +import { IsISO8601, IsNumber, IsUUID } from 'class-validator'; + +export class CreateAccountBalanceDto { + @IsUUID() + accountId: string; + + @IsNumber() + balance: number; + + @IsISO8601() + date: string; +} diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts index 537adf1d1..2c2036b16 100644 --- a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts +++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts @@ -140,15 +140,33 @@ export class AccountDetailDialog implements OnDestroy, OnInit { this.dialogRef.close(); } + public onAddAccountBalance({ + balance, + date + }: { + balance: number; + date: Date; + }) { + this.dataService + .postAccountBalance({ + balance, + date, + accountId: this.data.accountId + }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + this.fetchAccountBalances(); + this.fetchPortfolioPerformance(); + }); + } + public onDeleteAccountBalance(aId: string) { this.dataService .deleteAccountBalance(aId) .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe({ - next: () => { - this.fetchAccountBalances(); - this.fetchPortfolioPerformance(); - } + .subscribe(() => { + this.fetchAccountBalances(); + this.fetchPortfolioPerformance(); }); } diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html index 041a779c4..e092cce68 100644 --- a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html +++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html @@ -115,6 +115,7 @@ diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index a1d94c646..16d104834 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -42,7 +42,11 @@ import { translate } from '@ghostfolio/ui/i18n'; import { HttpClient, HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { SortDirection } from '@angular/material/sort'; -import { DataSource, Order as OrderModel } from '@prisma/client'; +import { + AccountBalance, + DataSource, + Order as OrderModel +} from '@prisma/client'; import { format, parseISO } from 'date-fns'; import { cloneDeep, groupBy, isNumber } from 'lodash'; import { Observable } from 'rxjs'; @@ -611,6 +615,22 @@ export class DataService { return this.http.post(`/api/v1/account`, aAccount); } + public postAccountBalance({ + accountId, + balance, + date + }: { + accountId: string; + balance: number; + date: Date; + }) { + return this.http.post(`/api/v1/account-balance`, { + accountId, + balance, + date + }); + } + public postBenchmark(benchmark: UniqueAsset) { return this.http.post(`/api/v1/benchmark`, benchmark); } diff --git a/libs/common/src/lib/permissions.ts b/libs/common/src/lib/permissions.ts index 09bbfa1bd..890cb8b63 100644 --- a/libs/common/src/lib/permissions.ts +++ b/libs/common/src/lib/permissions.ts @@ -7,6 +7,7 @@ export const permissions = { accessAssistant: 'accessAssistant', createAccess: 'createAccess', createAccount: 'createAccount', + createAccountBalance: 'createAccountBalance', createOrder: 'createOrder', createPlatform: 'createPlatform', createTag: 'createTag', @@ -47,6 +48,7 @@ export function getPermissions(aRole: Role): string[] { permissions.accessAssistant, permissions.createAccess, permissions.createAccount, + permissions.createAccountBalance, permissions.deleteAccountBalance, permissions.createOrder, permissions.createPlatform, @@ -75,6 +77,7 @@ export function getPermissions(aRole: Role): string[] { permissions.accessAssistant, permissions.createAccess, permissions.createAccount, + permissions.createAccountBalance, permissions.createOrder, permissions.deleteAccess, permissions.deleteAccount, diff --git a/libs/ui/src/lib/account-balances/account-balances.component.html b/libs/ui/src/lib/account-balances/account-balances.component.html index 0d1a79f41..0cfa4a5da 100644 --- a/libs/ui/src/lib/account-balances/account-balances.component.html +++ b/libs/ui/src/lib/account-balances/account-balances.component.html @@ -1,60 +1,106 @@ - - - - - + +
- Date - - -
+ + + + + - - - - + + + + + - - - + + - + + - - -
+ Date + + + + + + + + + + + - Value - -
- -
-
+ Value + +
+ +
+
+
+ + +
+ {{ accountCurrency }} +
+
+
+
- @if (showActions) { + + + @if (showActions) { + + } + + + + - } - - - -
+ + + + + diff --git a/libs/ui/src/lib/account-balances/account-balances.component.scss b/libs/ui/src/lib/account-balances/account-balances.component.scss index 5d4e87f30..0bf48a9e7 100644 --- a/libs/ui/src/lib/account-balances/account-balances.component.scss +++ b/libs/ui/src/lib/account-balances/account-balances.component.scss @@ -1,3 +1,10 @@ :host { display: block; } + +:host-context(.is-dark-theme) { + input { + color: rgb(var(--light-primary-text)); + background-color: rgb(var(--palette-foreground-text-light)); + } +} diff --git a/libs/ui/src/lib/account-balances/account-balances.component.ts b/libs/ui/src/lib/account-balances/account-balances.component.ts index 9d9e8ab19..c4fd379c8 100644 --- a/libs/ui/src/lib/account-balances/account-balances.component.ts +++ b/libs/ui/src/lib/account-balances/account-balances.component.ts @@ -14,7 +14,17 @@ import { Output, ViewChild } from '@angular/core'; +import { + FormGroup, + FormControl, + Validators, + ReactiveFormsModule +} from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; +import { DateAdapter } from '@angular/material/core'; +import { MatDatepickerModule } from '@angular/material/datepicker'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; import { MatMenuModule } from '@angular/material/menu'; import { MatSort, MatSortModule } from '@angular/material/sort'; import { MatTableDataSource, MatTableModule } from '@angular/material/table'; @@ -29,9 +39,13 @@ import { GfValueComponent } from '../value'; CommonModule, GfValueComponent, MatButtonModule, + MatDatepickerModule, + MatFormFieldModule, + MatInputModule, MatMenuModule, MatSortModule, - MatTableModule + MatTableModule, + ReactiveFormsModule ], schemas: [CUSTOM_ELEMENTS_SCHEMA], selector: 'gf-account-balances', @@ -43,24 +57,38 @@ export class GfAccountBalancesComponent implements OnChanges, OnDestroy, OnInit { @Input() accountBalances: AccountBalancesResponse['balances']; + @Input() accountCurrency: string; @Input() accountId: string; @Input() locale = getLocale(); @Input() showActions = true; + @Output() accountBalanceCreated = new EventEmitter<{ + balance: number; + date: Date; + }>(); @Output() accountBalanceDeleted = new EventEmitter(); @ViewChild(MatSort) sort: MatSort; + public accountBalanceForm = new FormGroup({ + balance: new FormControl(0, Validators.required), + date: new FormControl(new Date(), Validators.required) + }); + public dataSource: MatTableDataSource< AccountBalancesResponse['balances'][0] > = new MatTableDataSource(); + public displayedColumns: string[] = ['date', 'value', 'actions']; + public Validators = Validators; private unsubscribeSubject = new Subject(); - public constructor() {} + public constructor(private dateAdapter: DateAdapter) {} - public ngOnInit() {} + public ngOnInit() { + this.dateAdapter.setLocale(this.locale); + } public ngOnChanges() { if (this.accountBalances) { @@ -81,6 +109,10 @@ export class GfAccountBalancesComponent } } + public onSubmitAccountBalance() { + this.accountBalanceCreated.emit(this.accountBalanceForm.getRawValue()); + } + public ngOnDestroy() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete();