diff --git a/CHANGELOG.md b/CHANGELOG.md index 54b4eacd7..5cd20a1be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Improved the type filter of the activities table on the activities page to only list the activity types in use (experimental) - Improved the permission selector with icons in the create or update access dialog - Extracted the access level icon to a reusable component - Disabled the telemetry in the _Storybook_ setup +- Improved the indexes of the order database table - Upgraded the `Node.js` engine from version `>=22.18.0` to `>=22.22.3` (`package.json`) ### Fixed diff --git a/apps/api/src/app/portfolio/portfolio.service.spec.ts b/apps/api/src/app/portfolio/portfolio.service.spec.ts index 970b13734..7cc256cf6 100644 --- a/apps/api/src/app/portfolio/portfolio.service.spec.ts +++ b/apps/api/src/app/portfolio/portfolio.service.spec.ts @@ -247,7 +247,7 @@ describe('PortfolioService', () => { jest.spyOn(userService, 'user').mockResolvedValue({ accounts: [], - activityCount: 0, + activitiesCount: 0, dataProviderGhostfolioDailyRequests: 0, id: userDummyData.id, settings: { diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index 0a3a3ca76..c8220a824 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -129,8 +129,7 @@ export class UserService { const [ access, accounts, - activitiesCount, - firstActivity, + activitiesGroupedByType, impersonationUser, tagsForUser ] = await Promise.all([ @@ -150,13 +149,10 @@ export class UserService { userId: impersonationUserId || user.id } }), - this.prismaService.order.count({ - where: { userId: impersonationUserId || user.id } - }), - this.prismaService.order.findFirst({ - orderBy: { - date: 'asc' - }, + this.prismaService.order.groupBy({ + _min: { date: true }, + by: ['type'], + orderBy: { _min: { date: 'asc' } }, where: { userId: impersonationUserId || user.id } }), impersonationUserId @@ -165,6 +161,19 @@ export class UserService { this.tagService.getTagsForUser(impersonationUserId || user.id) ]); + const activitiesCount = impersonationUserId + ? (impersonationUser?.activitiesCount ?? 0) + : (user.activitiesCount ?? 0); + + const activityTypes = activitiesGroupedByType.map(({ type }) => { + return type; + }); + + // The groupBy is ordered by the minimum date, thus the first group + // carries the date of the first activity + const dateOfFirstActivity = + activitiesGroupedByType[0]?._min.date ?? new Date(); + const resolvedUserSettings = resolveUserSettings({ impersonationUserSettings: impersonationUser?.settings ?.settings as UserSettings, @@ -209,6 +218,8 @@ export class UserService { return { activitiesCount, + activityTypes, + dateOfFirstActivity, id, permissions, referralPartners, @@ -226,7 +237,6 @@ export class UserService { accounts: accounts.sort((a, b) => { return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); }), - dateOfFirstActivity: firstActivity?.date ?? new Date(), settings: { ...resolvedUserSettings, baseCurrency: resolvedUserSettings.baseCurrency ?? DEFAULT_CURRENCY, @@ -324,6 +334,7 @@ export class UserService { const user: UserWithSettings = { accessToken, accounts, + activitiesCount, authChallenge, createdAt, id, @@ -332,7 +343,6 @@ export class UserService { settings: settings as UserWithSettings['settings'], thirdPartyId, updatedAt, - activityCount: analytics?.activityCount, dataProviderGhostfolioDailyRequests: analytics?.dataProviderGhostfolioDailyRequests ?? 0 }; diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts index 22585efc6..33a0695aa 100644 --- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts +++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts @@ -649,11 +649,9 @@ export class GfHoldingDetailDialogComponent implements OnInit { .postActivity(activity) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { - this.router.navigate( - internalRoutes.portfolio.subRoutes.activities.routerLink - ); + this.userService.get(true).subscribe(); - this.dialogRef.close(); + this.navigateToActivities(); }); } @@ -729,4 +727,12 @@ export class GfHoldingDetailDialogComponent implements OnInit { { id: this.data.symbol, type: 'SYMBOL' } ]; } + + private navigateToActivities() { + this.router.navigate( + internalRoutes.portfolio.subRoutes.activities.routerLink + ); + + this.dialogRef.close(); + } } 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 e1c213183..102f58a31 100644 --- a/apps/client/src/app/pages/portfolio/activities/activities-page.html +++ b/apps/client/src/app/pages/portfolio/activities/activities-page.html @@ -3,6 +3,7 @@

Activities

[]; accounts: AccountWithPlatform[]; activitiesCount: number; + activityTypes: ActivityType[]; dateOfFirstActivity: Date; id: string; permissions: string[]; diff --git a/libs/common/src/lib/types/user-with-settings.type.ts b/libs/common/src/lib/types/user-with-settings.type.ts index 3773766b8..4c4a85a45 100644 --- a/libs/common/src/lib/types/user-with-settings.type.ts +++ b/libs/common/src/lib/types/user-with-settings.type.ts @@ -6,7 +6,7 @@ import { Account, Settings, User } from '@prisma/client'; // TODO: Compare with User interface export type UserWithSettings = User & { accounts: Account[]; - activityCount: number; + activitiesCount: number; dataProviderGhostfolioDailyRequests: number; permissions?: string[]; settings: Settings & { settings: UserSettings }; diff --git a/libs/ui/src/lib/activities-table/activities-table.component.html b/libs/ui/src/lib/activities-table/activities-table.component.html index 20c6e34b7..9bfd44b9d 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.html +++ b/libs/ui/src/lib/activities-table/activities-table.component.html @@ -2,14 +2,11 @@ class="align-items-center d-flex justify-content-end justify-content-lg-between" >
- @if (hasPermissionToFilterByType) { + @if (hasPermissionToFilterByType && activityTypeOptions().length > 1) { Type - @for ( - activityType of activityTypesTranslationMap | keyvalue: sortByValue; - track activityType.key - ) { + @for (activityType of activityTypeOptions(); track activityType.key) { {{ activityType.value }} diff --git a/libs/ui/src/lib/activities-table/activities-table.component.stories.ts b/libs/ui/src/lib/activities-table/activities-table.component.stories.ts index c5a0e58c2..b83555be1 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.stories.ts +++ b/libs/ui/src/lib/activities-table/activities-table.component.stories.ts @@ -483,6 +483,7 @@ export const Actions: Story = { export const Toolbar: Story = { args: { dataSource, + activityTypes: ['BUY', 'DIVIDEND', 'SELL'], baseCurrency: 'USD', deviceType: 'desktop', hasActivities: true, diff --git a/libs/ui/src/lib/activities-table/activities-table.component.ts b/libs/ui/src/lib/activities-table/activities-table.component.ts index b88eb0a29..acf17c933 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.ts +++ b/libs/ui/src/lib/activities-table/activities-table.component.ts @@ -141,7 +141,6 @@ export class GfActivitiesTableComponent implements AfterViewInit, OnInit { @ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatSort) sort: MatSort; - public activityTypesTranslationMap = new Map(); public hasDrafts = false; public hasErrors = false; public isDraftActivity = isDraftActivity; @@ -149,6 +148,7 @@ export class GfActivitiesTableComponent implements AfterViewInit, OnInit { public selectedRows = new SelectionModel(true, []); public typesFilter = new FormControl([]); + public readonly activityTypes = input([]); public readonly dataSource = input.required< MatTableDataSource | undefined >(); @@ -175,6 +175,16 @@ export class GfActivitiesTableComponent implements AfterViewInit, OnInit { return routerLinks; }); + protected readonly activityTypeOptions = computed(() => { + return (this.activityTypes() ?? []) + .map((activityType) => { + return { key: activityType, value: translate(activityType) }; + }) + .sort((a, b) => { + return a.value.localeCompare(b.value); + }); + }); + protected readonly displayedColumns = computed(() => { let columns = [ 'select', @@ -222,13 +232,6 @@ export class GfActivitiesTableComponent implements AfterViewInit, OnInit { private readonly notificationService = inject(NotificationService); public constructor(private destroyRef: DestroyRef) { - for (const type of Object.keys(ActivityType) as ActivityType[]) { - this.activityTypesTranslationMap.set( - ActivityType[type], - translate(ActivityType[type]) - ); - } - addIcons({ alertCircleOutline, calendarClearOutline, @@ -386,13 +389,6 @@ export class GfActivitiesTableComponent implements AfterViewInit, OnInit { }); } - public sortByValue( - a: { key: ActivityType; value: string }, - b: { key: ActivityType; value: string } - ) { - return a.value.localeCompare(b.value); - } - public toggleAllRows() { if (this.areAllRowsSelected()) { this.selectedRows.clear(); diff --git a/prisma/migrations/20260822000000_updated_indexes_of_order/migration.sql b/prisma/migrations/20260822000000_updated_indexes_of_order/migration.sql new file mode 100644 index 000000000..b9eb5ee55 --- /dev/null +++ b/prisma/migrations/20260822000000_updated_indexes_of_order/migration.sql @@ -0,0 +1,17 @@ +-- DropIndex +DROP INDEX "Order_symbolProfileId_idx"; + +-- DropIndex +DROP INDEX "Order_type_idx"; + +-- DropIndex +DROP INDEX "Order_userId_idx"; + +-- CreateIndex +CREATE INDEX "Order_symbolProfileId_date_idx" ON "Order"("symbolProfileId", "date"); + +-- CreateIndex +CREATE INDEX "Order_userId_date_idx" ON "Order"("userId", "date"); + +-- CreateIndex +CREATE INDEX "Order_userId_type_date_idx" ON "Order"("userId", "type", "date"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 41f1353c9..06ba23563 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -195,9 +195,9 @@ model Order { @@index([accountId]) @@index([date]) - @@index([symbolProfileId]) - @@index([type]) - @@index([userId]) + @@index([symbolProfileId, date]) + @@index([userId, date]) + @@index([userId, type, date]) } model Platform {