diff --git a/CHANGELOG.md b/CHANGELOG.md index c90de05cd..1d3a064cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Introduced the lazy-loaded activities table to the account detail dialog (experimental) - Introduced the lazy-loaded activities table to the import activities dialog (experimental) +- Introduced the lazy-loaded activities table to the position detail dialog (experimental) - Improved the font weight in the value component - Improved the language localization for Türkçe (`tr`) - Upgraded to _Inter_ 4 font family diff --git a/apps/api/src/app/export/export.controller.ts b/apps/api/src/app/export/export.controller.ts index ce02d9835..51b7bf632 100644 --- a/apps/api/src/app/export/export.controller.ts +++ b/apps/api/src/app/export/export.controller.ts @@ -20,6 +20,7 @@ export class ExportController { ): Promise { return this.exportService.export({ activityIds, + userCurrency: this.request.user.Settings.settings.baseCurrency, userId: this.request.user.id }); } diff --git a/apps/api/src/app/export/export.service.ts b/apps/api/src/app/export/export.service.ts index 2134a6520..031111d7f 100644 --- a/apps/api/src/app/export/export.service.ts +++ b/apps/api/src/app/export/export.service.ts @@ -13,9 +13,11 @@ export class ExportService { public async export({ activityIds, + userCurrency, userId }: { activityIds?: string[]; + userCurrency: string; userId: string; }): Promise { const accounts = ( @@ -39,10 +41,13 @@ export class ExportService { } ); - let activities = await this.orderService.orders({ - include: { SymbolProfile: true }, - orderBy: { date: 'desc' }, - where: { userId } + let { activities } = await this.orderService.getOrders({ + userCurrency, + userId, + includeDrafts: true, + sortColumn: 'date', + sortDirection: 'asc', + withExcludedAccounts: true }); if (activityIds) { diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index f89c57770..d15e8d437 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -236,6 +236,7 @@ export class ImportService { const activitiesExtendedWithErrors = await this.extendActivitiesWithErrors({ activitiesDto, + userCurrency, userId }); @@ -459,15 +460,18 @@ export class ImportService { private async extendActivitiesWithErrors({ activitiesDto, + userCurrency, userId }: { activitiesDto: Partial[]; + userCurrency: string; userId: string; }): Promise[]> { - const existingActivities = await this.orderService.orders({ - include: { SymbolProfile: true }, - orderBy: { date: 'desc' }, - where: { userId } + let { activities: existingActivities } = await this.orderService.getOrders({ + userCurrency, + userId, + includeDrafts: true, + withExcludedAccounts: true }); return activitiesDto.map( diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index 574bfdcd2..096aba6f5 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -25,7 +25,7 @@ import { endOfToday, isAfter } from 'date-fns'; import { groupBy } from 'lodash'; import { v4 as uuidv4 } from 'uuid'; -import { Activities, Activity } from './interfaces/activities.interface'; +import { Activities } from './interfaces/activities.interface'; @Injectable() export class OrderService { @@ -37,34 +37,6 @@ export class OrderService { private readonly symbolProfileService: SymbolProfileService ) {} - public async order( - orderWhereUniqueInput: Prisma.OrderWhereUniqueInput - ): Promise { - return this.prismaService.order.findUnique({ - where: orderWhereUniqueInput - }); - } - - public async orders(params: { - include?: Prisma.OrderInclude; - skip?: number; - take?: number; - cursor?: Prisma.OrderWhereUniqueInput; - where?: Prisma.OrderWhereInput; - orderBy?: Prisma.Enumerable; - }): Promise { - const { include, skip, take, cursor, where, orderBy } = params; - - return this.prismaService.order.findMany({ - cursor, - include, - orderBy, - skip, - take, - where - }); - } - public async createOrder( data: Prisma.OrderCreateInput & { accountId?: string; @@ -379,6 +351,14 @@ export class OrderService { return { activities, count }; } + public async order( + orderWhereUniqueInput: Prisma.OrderWhereUniqueInput + ): Promise { + return this.prismaService.order.findUnique({ + where: orderWhereUniqueInput + }); + } + public async updateOrder({ data, where @@ -455,4 +435,24 @@ export class OrderService { where }); } + + private async orders(params: { + include?: Prisma.OrderInclude; + skip?: number; + take?: number; + cursor?: Prisma.OrderWhereUniqueInput; + where?: Prisma.OrderWhereInput; + orderBy?: Prisma.Enumerable; + }): Promise { + const { include, skip, take, cursor, where, orderBy } = params; + + return this.prismaService.order.findMany({ + cursor, + include, + orderBy, + skip, + take, + where + }); + } } 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 aa835b00f..d37defe2f 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 @@ -7,6 +7,8 @@ import { OnInit } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Sort, SortDirection } from '@angular/material/sort'; +import { MatTableDataSource } from '@angular/material/table'; 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'; @@ -16,6 +18,7 @@ import { HistoricalDataItem, User } from '@ghostfolio/common/interfaces'; +import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { OrderWithAccount } from '@ghostfolio/common/types'; import Big from 'big.js'; import { format, parseISO } from 'date-fns'; @@ -24,7 +27,6 @@ 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' }, @@ -38,6 +40,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit { public activities: OrderWithAccount[]; public balance: number; public currency: string; + public dataSource: MatTableDataSource; public equity: number; public hasImpersonationId: boolean; public hasPermissionToDeleteAccountBalance: boolean; @@ -46,6 +49,9 @@ export class AccountDetailDialog implements OnDestroy, OnInit { public isLoadingChart: boolean; public name: string; public platformName: string; + public sortColumn = 'date'; + public sortDirection: SortDirection = 'desc'; + public totalItems: number; public transactionCount: number; public user: User; public valueInBaseCurrency: number; @@ -77,8 +83,6 @@ export class AccountDetailDialog implements OnDestroy, OnInit { } public ngOnInit() { - this.isLoadingActivities = true; - this.dataService .fetchAccount(this.data.accountId) .pipe(takeUntil(this.unsubscribeSubject)) @@ -110,19 +114,6 @@ export class AccountDetailDialog implements OnDestroy, OnInit { } ); - this.dataService - .fetchActivities({ - filters: [{ id: this.data.accountId, type: 'ACCOUNT' }] - }) - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(({ activities }) => { - this.activities = activities; - - this.isLoadingActivities = false; - - this.changeDetectorRef.markForCheck(); - }); - this.impersonationStorageService .onChangeHasImpersonation() .pipe(takeUntil(this.unsubscribeSubject)) @@ -131,6 +122,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit { }); this.fetchAccountBalances(); + this.fetchActivities(); this.fetchPortfolioPerformance(); } @@ -151,12 +143,20 @@ export class AccountDetailDialog implements OnDestroy, OnInit { } public onExport() { + let activityIds = []; + + if (this.user?.settings?.isExperimentalFeatures === true) { + activityIds = this.dataSource.data.map(({ id }) => { + return id; + }); + } else { + activityIds = this.activities.map(({ id }) => { + return id; + }); + } + this.dataService - .fetchExport( - this.activities.map(({ id }) => { - return id; - }) - ) + .fetchExport(activityIds) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((data) => { downloadAsFile({ @@ -172,6 +172,13 @@ export class AccountDetailDialog implements OnDestroy, OnInit { }); } + public onSortChanged({ active, direction }: Sort) { + this.sortColumn = active; + this.sortDirection = direction; + + this.fetchActivities(); + } + private fetchAccountBalances() { this.dataService .fetchAccountBalances(this.data.accountId) @@ -183,6 +190,41 @@ export class AccountDetailDialog implements OnDestroy, OnInit { }); } + private fetchActivities() { + this.isLoadingActivities = true; + + if (this.user?.settings?.isExperimentalFeatures === true) { + this.dataService + .fetchActivities({ + filters: [{ id: this.data.accountId, type: 'ACCOUNT' }], + sortColumn: this.sortColumn, + sortDirection: this.sortDirection + }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(({ activities, count }) => { + this.dataSource = new MatTableDataSource(activities); + this.totalItems = count; + + this.isLoadingActivities = false; + + this.changeDetectorRef.markForCheck(); + }); + } else { + this.dataService + .fetchActivities({ + filters: [{ id: this.data.accountId, type: 'ACCOUNT' }] + }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(({ activities }) => { + this.activities = activities; + + this.isLoadingActivities = false; + + this.changeDetectorRef.markForCheck(); + }); + } + } + private fetchPortfolioPerformance() { this.isLoadingChart = true; 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 647ba0d6f..1b9ba761b 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 @@ -71,7 +71,25 @@ > Activities + ; public dividendInBaseCurrency: number; public feeInBaseCurrency: number; public firstBuyDate: string; @@ -51,16 +57,19 @@ export class PositionDetailDialog implements OnDestroy, OnInit { public minPrice: number; public netPerformance: number; public netPerformancePercent: number; - public orders: OrderWithAccount[]; public quantity: number; public quantityPrecision = 2; public reportDataGlitchMail: string; public sectors: { [name: string]: { name: string; value: number }; }; + public sortColumn = 'date'; + public sortDirection: SortDirection = 'desc'; public SymbolProfile: EnhancedSymbolProfile; public tags: Tag[]; + public totalItems: number; public transactionCount: number; + public user: User; public value: number; private unsubscribeSubject = new Subject(); @@ -69,7 +78,8 @@ export class PositionDetailDialog implements OnDestroy, OnInit { private changeDetectorRef: ChangeDetectorRef, private dataService: DataService, public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: PositionDetailDialogParams + @Inject(MAT_DIALOG_DATA) public data: PositionDetailDialogParams, + private userService: UserService ) {} public ngOnInit(): void { @@ -102,10 +112,12 @@ export class PositionDetailDialog implements OnDestroy, OnInit { transactionCount, value }) => { + this.activities = orders; this.averagePrice = averagePrice; this.benchmarkDataItems = []; this.countries = {}; this.dataProviderInfo = dataProviderInfo; + this.dataSource = new MatTableDataSource(orders.reverse()); this.dividendInBaseCurrency = dividendInBaseCurrency; this.feeInBaseCurrency = feeInBaseCurrency; this.firstBuyDate = firstBuyDate; @@ -130,7 +142,6 @@ export class PositionDetailDialog implements OnDestroy, OnInit { this.minPrice = minPrice; this.netPerformance = netPerformance; this.netPerformancePercent = netPerformancePercent; - this.orders = orders; this.quantity = quantity; this.reportDataGlitchMail = `mailto:hi@ghostfol.io?Subject=Ghostfolio Data Glitch Report&body=Hello%0D%0DI would like to report a data glitch for%0D%0DSymbol: ${SymbolProfile?.symbol}%0DData Source: ${SymbolProfile?.dataSource}%0D%0DAdditional notes:%0D%0DCan you please take a look?%0D%0DKind regards`; this.sectors = {}; @@ -142,6 +153,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit { }; }); this.transactionCount = transactionCount; + this.totalItems = transactionCount; this.value = value; if (SymbolProfile?.assetClass) { @@ -239,6 +251,16 @@ export class PositionDetailDialog implements OnDestroy, OnInit { this.changeDetectorRef.markForCheck(); } ); + + this.userService.stateChanged + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((state) => { + if (state?.user) { + this.user = state.user; + + this.changeDetectorRef.markForCheck(); + } + }); } public onClose(): void { @@ -246,12 +268,20 @@ export class PositionDetailDialog implements OnDestroy, OnInit { } public onExport() { + let activityIds = []; + + if (this.user?.settings?.isExperimentalFeatures === true) { + activityIds = this.dataSource.data.map(({ id }) => { + return id; + }); + } else { + activityIds = this.activities.map(({ id }) => { + return id; + }); + } + this.dataService - .fetchExport( - this.orders.map((order) => { - return order.id; - }) - ) + .fetchExport(activityIds) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((data) => { downloadAsFile({ diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html index 8d0f62ed9..f7b9b8a80 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -246,11 +246,30 @@ -
+
Activities
+
diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.module.ts b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.module.ts index 4c9a4a923..46ae87f66 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.module.ts +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.module.ts @@ -5,6 +5,7 @@ import { MatChipsModule } from '@angular/material/chips'; import { MatDialogModule } from '@angular/material/dialog'; import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module'; import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module'; +import { GfActivitiesTableLazyModule } from '@ghostfolio/ui/activities-table-lazy/activities-table-lazy.module'; import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module'; import { GfDataProviderCreditsModule } from '@ghostfolio/ui/data-provider-credits/data-provider-credits.module'; import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module'; @@ -19,6 +20,7 @@ import { PositionDetailDialog } from './position-detail-dialog.component'; imports: [ CommonModule, GfActivitiesTableModule, + GfActivitiesTableLazyModule, GfDataProviderCreditsModule, GfDialogFooterModule, GfDialogHeaderModule, diff --git a/libs/ui/src/lib/activities-table-lazy/activities-table-lazy.component.html b/libs/ui/src/lib/activities-table-lazy/activities-table-lazy.component.html index 1173c4854..460f218e1 100644 --- a/libs/ui/src/lib/activities-table-lazy/activities-table-lazy.component.html +++ b/libs/ui/src/lib/activities-table-lazy/activities-table-lazy.component.html @@ -72,6 +72,7 @@ [dataSource]="dataSource" [matSortActive]="sortColumn" [matSortDirection]="sortDirection" + [matSortDisabled]="sortDisabled" > diff --git a/libs/ui/src/lib/activities-table-lazy/activities-table-lazy.component.ts b/libs/ui/src/lib/activities-table-lazy/activities-table-lazy.component.ts index caab993e0..950df149b 100644 --- a/libs/ui/src/lib/activities-table-lazy/activities-table-lazy.component.ts +++ b/libs/ui/src/lib/activities-table-lazy/activities-table-lazy.component.ts @@ -48,6 +48,7 @@ export class ActivitiesTableLazyComponent @Input() showNameColumn = true; @Input() sortColumn: string; @Input() sortDirection: SortDirection; + @Input() sortDisabled = false; @Input() totalItems = Number.MAX_SAFE_INTEGER; @Output() activityDeleted = new EventEmitter();