diff --git a/CHANGELOG.md b/CHANGELOG.md index 52a4a6e1c..120bcc3b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added a historical cash balances table to the account detail dialog - Introduced a `HasPermission` annotation for endpoints +### Changed + +- Respected the `withExcludedAccounts` flag in the account balance time series + ## 2.27.1 - 2023-11-28 ### Changed diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index b5bc589e6..76aef0db1 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -1075,7 +1075,7 @@ export class PortfolioService { const userCurrency = this.getUserCurrency(user); const accountBalances = await this.accountBalanceService.getAccountBalances( - { filters, user } + { filters, user, withExcludedAccounts } ); let accountBalanceItems: HistoricalDataItem[] = Object.values( diff --git a/apps/api/src/services/account-balance/account-balance.service.ts b/apps/api/src/services/account-balance/account-balance.service.ts index 33b811ef5..e1d002428 100644 --- a/apps/api/src/services/account-balance/account-balance.service.ts +++ b/apps/api/src/services/account-balance/account-balance.service.ts @@ -22,10 +22,12 @@ export class AccountBalanceService { public async getAccountBalances({ filters, - user + user, + withExcludedAccounts }: { filters?: Filter[]; user: UserWithSettings; + withExcludedAccounts?: boolean; }): Promise { const where: Prisma.AccountBalanceWhereInput = { userId: user.id }; @@ -37,6 +39,10 @@ export class AccountBalanceService { where.accountId = accountFilter.id; } + if (withExcludedAccounts === false) { + where.Account = { isExcluded: false }; + } + const balances = await this.prismaService.accountBalance.findMany({ where, orderBy: { 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 284bfcca2..b3a916da9 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 @@ -29,14 +29,15 @@ import { AccountDetailDialogParams } from './interfaces/interfaces'; styleUrls: ['./account-detail-dialog.component.scss'] }) export class AccountDetailDialog implements OnDestroy, OnInit { + public activities: OrderWithAccount[]; public balance: number; public currency: string; public equity: number; public hasImpersonationId: boolean; public historicalDataItems: HistoricalDataItem[]; + public isLoadingActivities: boolean; public isLoadingChart: boolean; public name: string; - public orders: OrderWithAccount[]; public platformName: string; public transactionCount: number; public user: User; @@ -64,6 +65,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit { } public ngOnInit() { + this.isLoadingActivities = true; this.isLoadingChart = true; this.dataService @@ -103,7 +105,9 @@ export class AccountDetailDialog implements OnDestroy, OnInit { }) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(({ activities }) => { - this.orders = activities; + this.activities = activities; + + this.isLoadingActivities = false; this.changeDetectorRef.markForCheck(); }); @@ -153,8 +157,8 @@ export class AccountDetailDialog implements OnDestroy, OnInit { public onExport() { this.dataService .fetchExport( - this.orders.map((order) => { - return order.id; + this.activities.map(({ id }) => { + return id; }) ) .pipe(takeUntil(this.unsubscribeSubject)) 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 02d1c917e..7e92eca85 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 @@ -31,7 +31,7 @@ > -
+
-
-
-
Activities
+ + + Activities -
-
+ + + Cash Balances + + +
diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.module.ts b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.module.ts index c3d45b6ce..83ac5b6ea 100644 --- a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.module.ts +++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.module.ts @@ -2,9 +2,11 @@ import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatDialogModule } from '@angular/material/dialog'; +import { MatTabsModule } from '@angular/material/tabs'; import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module'; import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module'; import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module'; +import { GfAccountBalancesModule } from '@ghostfolio/ui/account-balances/account-balances.module'; import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module'; import { GfValueModule } from '@ghostfolio/ui/value'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; @@ -15,6 +17,7 @@ import { AccountDetailDialog } from './account-detail-dialog.component'; declarations: [AccountDetailDialog], imports: [ CommonModule, + GfAccountBalancesModule, GfActivitiesTableModule, GfDialogFooterModule, GfDialogHeaderModule, @@ -22,6 +25,7 @@ import { AccountDetailDialog } from './account-detail-dialog.component'; GfValueModule, MatButtonModule, MatDialogModule, + MatTabsModule, NgxSkeletonLoaderModule ], schemas: [CUSTOM_ELEMENTS_SCHEMA] diff --git a/apps/client/src/app/pages/portfolio/activities/activities-page.html b/apps/client/src/app/pages/portfolio/activities/activities-page.html index a5c9201a0..8c2cf9bd5 100644 --- a/apps/client/src/app/pages/portfolio/activities/activities-page.html +++ b/apps/client/src/app/pages/portfolio/activities/activities-page.html @@ -1,5 +1,5 @@
-
+

Activities

(`/api/v1/account/${aAccountId}`); } + public fetchAccountBalances(aAccountId: string) { + return this.http.get( + `/api/v1/account/${aAccountId}/balances` + ); + } + public fetchAccounts() { return this.http.get('/api/v1/account'); } diff --git a/apps/client/src/assets/oss-friends.json b/apps/client/src/assets/oss-friends.json index a95f8e709..d22f4c030 100644 --- a/apps/client/src/assets/oss-friends.json +++ b/apps/client/src/assets/oss-friends.json @@ -1,5 +1,5 @@ { - "createdAt": "2023-11-17T00:00:00.000Z", + "createdAt": "2023-11-30T00:00:00.000Z", "data": [ { "name": "BoxyHQ", @@ -16,6 +16,11 @@ "description": "Centralize community, product, and customer data to understand which companies are engaging with your open source project.", "href": "https://www.crowd.dev" }, + { + "name": "DevHunt", + "description": "Find the best Dev Tools upvoted by the community every week.", + "href": "https://devhunt.org" + }, { "name": "Documenso", "description": "The Open-Source DocuSign Alternative. We aim to earn your trust by enabling you to self-host the platform and examine its inner workings.", @@ -59,7 +64,7 @@ { "name": "Hook0", "description": "Open-Source Webhooks-as-a-service (WaaS) that makes it easy for developers to send webhooks.", - "href": "https://www.hook0.com/" + "href": "https://www.hook0.com" }, { "name": "HTMX", @@ -89,7 +94,7 @@ { "name": "Papermark", "description": "Open-Source Docsend Alternative to securely share documents with real-time analytics.", - "href": "https://www.papermark.io/" + "href": "https://www.papermark.io" }, { "name": "Requestly", @@ -109,7 +114,7 @@ { "name": "Shelf.nu", "description": "Open Source Asset and Equipment tracking software that lets you create QR asset labels, manage and overview your assets across locations.", - "href": "https://www.shelf.nu/" + "href": "https://www.shelf.nu" }, { "name": "Sniffnet", diff --git a/libs/ui/src/lib/account-balances/account-balances.component.html b/libs/ui/src/lib/account-balances/account-balances.component.html new file mode 100644 index 000000000..81f8a8192 --- /dev/null +++ b/libs/ui/src/lib/account-balances/account-balances.component.html @@ -0,0 +1,36 @@ + + + + + + + + + + + + + +
+ Date + + + + Value + +
+ +
+
diff --git a/libs/ui/src/lib/account-balances/account-balances.component.scss b/libs/ui/src/lib/account-balances/account-balances.component.scss new file mode 100644 index 000000000..b5b58f67e --- /dev/null +++ b/libs/ui/src/lib/account-balances/account-balances.component.scss @@ -0,0 +1,5 @@ +@import 'apps/client/src/styles/ghostfolio-style'; + +:host { + display: block; +} diff --git a/libs/ui/src/lib/account-balances/account-balances.component.ts b/libs/ui/src/lib/account-balances/account-balances.component.ts new file mode 100644 index 000000000..c552519d6 --- /dev/null +++ b/libs/ui/src/lib/account-balances/account-balances.component.ts @@ -0,0 +1,63 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + Input, + OnDestroy, + OnInit, + 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'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'gf-account-balances', + styleUrls: ['./account-balances.component.scss'], + templateUrl: './account-balances.component.html' +}) +export class AccountBalancesComponent implements OnDestroy, OnInit { + @Input() accountId: string; + @Input() locale: string; + + @ViewChild(MatSort) sort: MatSort; + + public dataSource: MatTableDataSource< + AccountBalancesResponse['balances'][0] + > = new MatTableDataSource(); + public displayedColumns: string[] = ['date', 'value']; + + private unsubscribeSubject = new Subject(); + + public constructor( + private changeDetectorRef: ChangeDetectorRef, + private dataService: DataService + ) {} + + public ngOnInit() { + this.fetchBalances(); + } + + 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(); + }); + } +} diff --git a/libs/ui/src/lib/account-balances/account-balances.module.ts b/libs/ui/src/lib/account-balances/account-balances.module.ts new file mode 100644 index 000000000..cc8fb9677 --- /dev/null +++ b/libs/ui/src/lib/account-balances/account-balances.module.ts @@ -0,0 +1,15 @@ +import { CommonModule } from '@angular/common'; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; +import { MatSortModule } from '@angular/material/sort'; +import { MatTableModule } from '@angular/material/table'; +import { GfValueModule } from '@ghostfolio/ui/value'; + +import { AccountBalancesComponent } from './account-balances.component'; + +@NgModule({ + declarations: [AccountBalancesComponent], + exports: [AccountBalancesComponent], + imports: [CommonModule, GfValueModule, MatSortModule, MatTableModule], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class GfAccountBalancesModule {}