From c2ce4609ec7720a9c6d31fb212eb59e3bd934bd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Mart=C3=ADn?= Date: Tue, 21 Oct 2025 19:45:48 +0200 Subject: [PATCH 01/14] Task/add filter functionality to access dialog component --- ...reate-or-update-access-dialog.component.ts | 80 +++++++++++++++++++ .../create-or-update-access-dialog.html | 47 +++++++++-- .../create-or-update-access-dialog.scss | 6 ++ 3 files changed, 127 insertions(+), 6 deletions(-) diff --git a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts index 315f86244..7a1637949 100644 --- a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts +++ b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts @@ -3,11 +3,15 @@ import { UpdateAccessDto } from '@ghostfolio/api/app/access/update-access.dto'; import { NotificationService } from '@ghostfolio/client/core/notification/notification.service'; import { DataService } from '@ghostfolio/client/services/data.service'; import { validateObjectForForm } from '@ghostfolio/client/util/form.util'; +import { Filter, PortfolioPosition } from '@ghostfolio/common/interfaces'; +import { AccountWithPlatform } from '@ghostfolio/common/types'; +import { GfPortfolioFilterFormComponent } from '@ghostfolio/ui/portfolio-filter-form'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, + CUSTOM_ELEMENTS_SCHEMA, Inject, OnDestroy, OnInit @@ -28,7 +32,10 @@ import { import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; +import { IonIcon } from '@ionic/angular/standalone'; import { StatusCodes } from 'http-status-codes'; +import { addIcons } from 'ionicons'; +import { chevronUpOutline, optionsOutline } from 'ionicons/icons'; import { EMPTY, Subject, catchError, takeUntil } from 'rxjs'; import { CreateOrUpdateAccessDialogParams } from './interfaces/interfaces'; @@ -38,6 +45,8 @@ import { CreateOrUpdateAccessDialogParams } from './interfaces/interfaces'; host: { class: 'h-100' }, imports: [ FormsModule, + GfPortfolioFilterFormComponent, + IonIcon, MatButtonModule, MatDialogModule, MatFormFieldModule, @@ -45,6 +54,7 @@ import { CreateOrUpdateAccessDialogParams } from './interfaces/interfaces'; MatSelectModule, ReactiveFormsModule ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], selector: 'gf-create-or-update-access-dialog', styleUrls: ['./create-or-update-access-dialog.scss'], templateUrl: 'create-or-update-access-dialog.html' @@ -54,6 +64,14 @@ export class GfCreateOrUpdateAccessDialogComponent { public accessForm: FormGroup; public mode: 'create' | 'update'; + public showFilterPanel = false; + public filterPanelExpanded = false; + + // Datos para el filtro + public accounts: AccountWithPlatform[] = []; + public assetClasses: Filter[] = []; + public holdings: PortfolioPosition[] = []; + public tags: Filter[] = []; private unsubscribeSubject = new Subject(); @@ -66,6 +84,8 @@ export class GfCreateOrUpdateAccessDialogComponent private notificationService: NotificationService ) { this.mode = this.data.access?.id ? 'update' : 'create'; + + addIcons({ chevronUpOutline, optionsOutline }); } public ngOnInit() { @@ -73,6 +93,7 @@ export class GfCreateOrUpdateAccessDialogComponent this.accessForm = this.formBuilder.group({ alias: [this.data.access.alias], + filter: [null], granteeUserId: [ this.data.access.grantee, isPublic ? null : Validators.required @@ -87,19 +108,30 @@ export class GfCreateOrUpdateAccessDialogComponent this.accessForm.get('type').valueChanges.subscribe((accessType) => { const granteeUserIdControl = this.accessForm.get('granteeUserId'); const permissionsControl = this.accessForm.get('permissions'); + const filterControl = this.accessForm.get('filter'); if (accessType === 'PRIVATE') { granteeUserIdControl.setValidators(Validators.required); + this.showFilterPanel = false; + filterControl.setValue(null); } else { granteeUserIdControl.clearValidators(); granteeUserIdControl.setValue(null); permissionsControl.setValue(this.data.access.permissions[0]); + this.showFilterPanel = true; + this.loadFilterData(); } granteeUserIdControl.updateValueAndValidity(); this.changeDetectorRef.markForCheck(); }); + + // Si ya es público al iniciar, mostrar el panel y cargar datos + if (isPublic) { + this.showFilterPanel = true; + this.loadFilterData(); + } } public onCancel() { @@ -119,6 +151,54 @@ export class GfCreateOrUpdateAccessDialogComponent this.unsubscribeSubject.complete(); } + private loadFilterData() { + // Cargar cuentas + this.dataService + .fetchAccounts() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((response) => { + this.accounts = response.accounts; + this.changeDetectorRef.markForCheck(); + }); + + // Cargar holdings y asset classes + this.dataService + .fetchPortfolioDetails({}) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((response) => { + if (response.holdings) { + this.holdings = Object.values(response.holdings); + + // Extraer asset classes únicas + const assetClassesSet = new Set(); + Object.values(response.holdings).forEach((holding) => { + if (holding.assetClass) { + assetClassesSet.add(holding.assetClass); + } + }); + this.assetClasses = Array.from(assetClassesSet).map((ac) => ({ + id: ac, + label: ac, + type: 'ASSET_CLASS' as const + })); + } + this.changeDetectorRef.markForCheck(); + }); + + // Cargar tags + this.dataService + .fetchTags() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((response) => { + this.tags = response.map((tag) => ({ + id: tag.id, + label: tag.name, + type: 'TAG' as const + })); + this.changeDetectorRef.markForCheck(); + }); + } + private async createAccess() { const access: CreateAccessDto = { alias: this.accessForm.get('alias').value, diff --git a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html index 11669041d..4a7730484 100644 --- a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html +++ b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -4,13 +4,48 @@ (keyup.enter)="accessForm.valid && onSubmit()" (ngSubmit)="onSubmit()" > -

- @if (mode === 'create') { - Grant access - } @else { - Edit access +
+
+

+ @if (mode === 'create') { + Grant access + } @else { + Edit access + } +

+ @if (showFilterPanel) { + + } +
+ @if (showFilterPanel && filterPanelExpanded) { +
+

+ Configure which accounts, holdings, tags, and asset classes will be + visible in this public access. Leave empty to show all data. +

+ +
} -

+
diff --git a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.scss b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.scss index b63df0134..db06a805f 100644 --- a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.scss +++ b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.scss @@ -4,4 +4,10 @@ .mat-mdc-dialog-content { max-height: unset; } + + ion-icon { + &.rotate-90 { + transform: rotate(90deg); + } + } } From 17a4f313e12b021c5a58f747b8e4672ca5a264e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Mart=C3=ADn?= Date: Wed, 22 Oct 2025 18:09:55 +0200 Subject: [PATCH 02/14] Task/refactor access dialog to enhance filter settings and improve form structure --- ...reate-or-update-access-dialog.component.ts | 58 +++++---- .../create-or-update-access-dialog.html | 121 ++++++++++++------ 2 files changed, 110 insertions(+), 69 deletions(-) diff --git a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts index 7a1637949..33be69382 100644 --- a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts +++ b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts @@ -1,22 +1,18 @@ import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto'; import { UpdateAccessDto } from '@ghostfolio/api/app/access/update-access.dto'; -import { NotificationService } from '@ghostfolio/client/core/notification/notification.service'; -import { DataService } from '@ghostfolio/client/services/data.service'; -import { validateObjectForForm } from '@ghostfolio/client/util/form.util'; import { Filter, PortfolioPosition } from '@ghostfolio/common/interfaces'; import { AccountWithPlatform } from '@ghostfolio/common/types'; -import { GfPortfolioFilterFormComponent } from '@ghostfolio/ui/portfolio-filter-form'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, - CUSTOM_ELEMENTS_SCHEMA, Inject, OnDestroy, OnInit } from '@angular/core'; import { + AbstractControl, FormBuilder, FormGroup, FormsModule, @@ -32,12 +28,13 @@ import { import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; -import { IonIcon } from '@ionic/angular/standalone'; +import { AccessPermission } from '@prisma/client'; import { StatusCodes } from 'http-status-codes'; -import { addIcons } from 'ionicons'; -import { chevronUpOutline, optionsOutline } from 'ionicons/icons'; import { EMPTY, Subject, catchError, takeUntil } from 'rxjs'; +import { NotificationService } from '../../../core/notification/notification.service'; +import { DataService } from '../../../services/data.service'; +import { validateObjectForForm } from '../../../util/form.util'; import { CreateOrUpdateAccessDialogParams } from './interfaces/interfaces'; @Component({ @@ -45,8 +42,6 @@ import { CreateOrUpdateAccessDialogParams } from './interfaces/interfaces'; host: { class: 'h-100' }, imports: [ FormsModule, - GfPortfolioFilterFormComponent, - IonIcon, MatButtonModule, MatDialogModule, MatFormFieldModule, @@ -54,7 +49,6 @@ import { CreateOrUpdateAccessDialogParams } from './interfaces/interfaces'; MatSelectModule, ReactiveFormsModule ], - schemas: [CUSTOM_ELEMENTS_SCHEMA], selector: 'gf-create-or-update-access-dialog', styleUrls: ['./create-or-update-access-dialog.scss'], templateUrl: 'create-or-update-access-dialog.html' @@ -65,7 +59,6 @@ export class GfCreateOrUpdateAccessDialogComponent public accessForm: FormGroup; public mode: 'create' | 'update'; public showFilterPanel = false; - public filterPanelExpanded = false; // Datos para el filtro public accounts: AccountWithPlatform[] = []; @@ -84,8 +77,6 @@ export class GfCreateOrUpdateAccessDialogComponent private notificationService: NotificationService ) { this.mode = this.data.access?.id ? 'update' : 'create'; - - addIcons({ chevronUpOutline, optionsOutline }); } public ngOnInit() { @@ -96,12 +87,17 @@ export class GfCreateOrUpdateAccessDialogComponent filter: [null], granteeUserId: [ this.data.access.grantee, - isPublic ? null : Validators.required + isPublic + ? null + : [(control: AbstractControl) => Validators.required(control)] + ], + permissions: [ + this.data.access.permissions[0], + [(control: AbstractControl) => Validators.required(control)] ], - permissions: [this.data.access.permissions[0], Validators.required], type: [ { disabled: this.mode === 'update', value: this.data.access.type }, - Validators.required + [(control: AbstractControl) => Validators.required(control)] ] }); @@ -111,7 +107,9 @@ export class GfCreateOrUpdateAccessDialogComponent const filterControl = this.accessForm.get('filter'); if (accessType === 'PRIVATE') { - granteeUserIdControl.setValidators(Validators.required); + granteeUserIdControl.setValidators([ + (control: AbstractControl) => Validators.required(control) + ]); this.showFilterPanel = false; filterControl.setValue(null); } else { @@ -201,9 +199,11 @@ export class GfCreateOrUpdateAccessDialogComponent private async createAccess() { const access: CreateAccessDto = { - alias: this.accessForm.get('alias').value, - granteeUserId: this.accessForm.get('granteeUserId').value, - permissions: [this.accessForm.get('permissions').value] + alias: this.accessForm.get('alias')?.value as string, + granteeUserId: this.accessForm.get('granteeUserId')?.value as string, + permissions: [ + this.accessForm.get('permissions')?.value as AccessPermission + ] }; try { @@ -216,8 +216,8 @@ export class GfCreateOrUpdateAccessDialogComponent this.dataService .postAccess(access) .pipe( - catchError((error) => { - if (error.status === StatusCodes.BAD_REQUEST) { + catchError((error: { status?: number }) => { + if (error?.status === StatusCodes.BAD_REQUEST) { this.notificationService.alert({ title: $localize`Oops! Could not grant access.` }); @@ -237,10 +237,12 @@ export class GfCreateOrUpdateAccessDialogComponent private async updateAccess() { const access: UpdateAccessDto = { - alias: this.accessForm.get('alias').value, - granteeUserId: this.accessForm.get('granteeUserId').value, + alias: this.accessForm.get('alias')?.value as string, + granteeUserId: this.accessForm.get('granteeUserId')?.value as string, id: this.data.access.id, - permissions: [this.accessForm.get('permissions').value] + permissions: [ + this.accessForm.get('permissions')?.value as AccessPermission + ] }; try { @@ -253,8 +255,8 @@ export class GfCreateOrUpdateAccessDialogComponent this.dataService .putAccess(access) .pipe( - catchError(({ status }) => { - if (status.status === StatusCodes.BAD_REQUEST) { + catchError((error: { status?: number }) => { + if (error?.status === StatusCodes.BAD_REQUEST) { this.notificationService.alert({ title: $localize`Oops! Could not update access.` }); diff --git a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html index 4a7730484..42001c3b4 100644 --- a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html +++ b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html @@ -4,48 +4,13 @@ (keyup.enter)="accessForm.valid && onSubmit()" (ngSubmit)="onSubmit()" > -
-
-

- @if (mode === 'create') { - Grant access - } @else { - Edit access - } -

- @if (showFilterPanel) { - - } -
- @if (showFilterPanel && filterPanelExpanded) { -
-

- Configure which accounts, holdings, tags, and asset classes will be - visible in this public access. Leave empty to show all data. -

- -
+

+ @if (mode === 'create') { + Grant access + } @else { + Edit access } -

+
@@ -94,6 +59,80 @@
} + + @if (showFilterPanel) { +
+

+ Filter Settings (Optional) +

+

+ Configure which accounts, holdings, tags, and asset classes will be + visible in this public access. Leave empty to show all data. +

+ +
+ + Account + + + @for (account of accounts; track account.id) { + + {{ account.name }} + + } + + +
+ +
+ + Holding + + + @for (holding of holdings; track holding.symbol) { + +
+ {{ holding.name }} +
+ {{ holding.symbol }} · {{ holding.currency }} +
+
+ } +
+
+
+ +
+ + Tag + + + @for (tag of tags; track tag.id) { + {{ tag.label }} + } + + +
+ +
+ + Asset Class + + + @for (assetClass of assetClasses; track assetClass.id) { + {{ + assetClass.label + }} + } + + +
+
+ }
diff --git a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.scss b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.scss index db06a805f..ac08fa42d 100644 --- a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.scss +++ b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.scss @@ -10,4 +10,15 @@ transform: rotate(90deg); } } + + .filter-settings-title { + font-size: 1.125rem; + font-weight: 500; + margin-bottom: 1rem; + } + + .filter-settings-description { + font-size: 0.875rem; + margin-bottom: 1rem; + } } From f3f82b7311012ac6d3a7d255173c04bc3652bcd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Mart=C3=ADn?= Date: Fri, 31 Oct 2025 19:10:56 +0100 Subject: [PATCH 10/14] Feature: Refactor holding filters in public portfolio retrieval --- .../app/endpoints/public/public.controller.ts | 27 +++++-------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/apps/api/src/app/endpoints/public/public.controller.ts b/apps/api/src/app/endpoints/public/public.controller.ts index f8622ebbc..ddb2bd097 100644 --- a/apps/api/src/app/endpoints/public/public.controller.ts +++ b/apps/api/src/app/endpoints/public/public.controller.ts @@ -106,18 +106,18 @@ export class PublicController { // Add holding filters (symbol + dataSource) // Each holding needs both DATA_SOURCE and SYMBOL filters if (accessFilter.holdings?.length > 0) { - accessFilter.holdings.forEach((holding) => { + for (const { dataSource, symbol } of accessFilter.holdings) { portfolioFilters.push( { - id: holding.dataSource, + id: dataSource, type: 'DATA_SOURCE' as const }, { - id: holding.symbol, + id: symbol, type: 'SYMBOL' as const } ); - }); + } } } @@ -185,33 +185,18 @@ export class PublicController { includeDrafts: false, sortColumn: 'date', sortDirection: 'desc', - take: hasMultipleHoldingFilters ? 1000 : 10, // Get more if we need to filter manually + take: 10, types: [ActivityType.BUY, ActivityType.SELL], userCurrency: user.settings?.settings.baseCurrency ?? DEFAULT_CURRENCY, userId: user.id, withExcludedAccountsAndActivities: false }); - let filteredActivities = activities; - if (hasMultipleHoldingFilters && accessFilter.holdings) { - filteredActivities = activities.filter((activity) => { - return accessFilter.holdings.some( - (holding) => - activity.SymbolProfile.dataSource === holding.dataSource && - activity.SymbolProfile.symbol === holding.symbol - ); - }); - } - - // Take only the latest 10 activities after filtering - const latestActivitiesData = filteredActivities.slice(0, 10); - - // Experimental const latestActivities = this.configurationService.get( 'ENABLE_FEATURE_SUBSCRIPTION' ) ? [] - : latestActivitiesData.map( + : activities.map( ({ currency, date, From 0a2ff009c24d4a5b0d9d079533c237a90a5199cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Mart=C3=ADn?= Date: Fri, 31 Oct 2025 19:28:55 +0100 Subject: [PATCH 11/14] Feature: Rearrange properties in public portfolio response structure --- .../src/app/endpoints/public/public.controller.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/api/src/app/endpoints/public/public.controller.ts b/apps/api/src/app/endpoints/public/public.controller.ts index ddb2bd097..0f1a48a9b 100644 --- a/apps/api/src/app/endpoints/public/public.controller.ts +++ b/apps/api/src/app/endpoints/public/public.controller.ts @@ -227,12 +227,7 @@ export class PublicController { }); const publicPortfolioResponse: PublicPortfolioResponse = { - alias: access.alias, createdAt, - hasDetails, - latestActivities, - holdings: {}, - markets, performance: { '1d': { relativeChange: @@ -246,7 +241,12 @@ export class PublicController { relativeChange: performanceYtd.netPerformancePercentageWithCurrencyEffect } - } + }, + alias: access.alias, + hasDetails, + holdings: {}, + latestActivities, + markets }; const totalValue = getSum( From 11487459ee4b0adad37ed833270d1107f5afc438 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Mart=C3=ADn?= Date: Fri, 31 Oct 2025 19:29:14 +0100 Subject: [PATCH 12/14] Feature: Update Access interfaces to use AssetProfileIdentifier for holdings --- apps/api/src/app/access/access.controller.ts | 13 +------------ .../create-or-update-access-dialog.component.ts | 12 ++++++++---- libs/common/src/lib/interfaces/access.interface.ts | 4 +++- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/apps/api/src/app/access/access.controller.ts b/apps/api/src/app/access/access.controller.ts index e114e14f3..cf1c2c1e4 100644 --- a/apps/api/src/app/access/access.controller.ts +++ b/apps/api/src/app/access/access.controller.ts @@ -1,7 +1,7 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; -import { Access } from '@ghostfolio/common/interfaces'; +import { Access, AccessSettings } from '@ghostfolio/common/interfaces'; import { permissions } from '@ghostfolio/common/permissions'; import type { RequestWithUser } from '@ghostfolio/common/types'; @@ -26,17 +26,6 @@ import { AccessService } from './access.service'; import { CreateAccessDto } from './create-access.dto'; import { UpdateAccessDto } from './update-access.dto'; -interface AccessFilter { - accountIds?: string[]; - assetClasses?: string[]; - holdings?: { dataSource: string; symbol: string }[]; - tagIds?: string[]; -} - -interface AccessSettings { - filter?: AccessFilter; -} - @Controller('access') export class AccessController { public constructor( diff --git a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts index e6c14b623..ec63c268a 100644 --- a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts +++ b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts @@ -1,6 +1,10 @@ import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto'; import { UpdateAccessDto } from '@ghostfolio/api/app/access/update-access.dto'; -import { Filter, PortfolioPosition } from '@ghostfolio/common/interfaces'; +import { + AssetProfileIdentifier, + Filter, + PortfolioPosition +} from '@ghostfolio/common/interfaces'; import { AccountWithPlatform } from '@ghostfolio/common/types'; import { GfPortfolioFilterFormComponent, @@ -155,7 +159,7 @@ export class GfCreateOrUpdateAccessDialogComponent | { accountIds?: string[]; assetClasses?: string[]; - holdings?: { dataSource: string; symbol: string }[]; + holdings?: AssetProfileIdentifier[]; tagIds?: string[]; } | undefined { @@ -175,7 +179,7 @@ export class GfCreateOrUpdateAccessDialogComponent const filter: { accountIds?: string[]; assetClasses?: string[]; - holdings?: { dataSource: string; symbol: string }[]; + holdings?: AssetProfileIdentifier[]; tagIds?: string[]; } = {}; @@ -262,7 +266,7 @@ export class GfCreateOrUpdateAccessDialogComponent | { accountIds?: string[]; assetClasses?: string[]; - holdings?: { dataSource: string; symbol: string }[]; + holdings?: AssetProfileIdentifier[]; tagIds?: string[]; } | undefined diff --git a/libs/common/src/lib/interfaces/access.interface.ts b/libs/common/src/lib/interfaces/access.interface.ts index cd65e6813..e380c04ed 100644 --- a/libs/common/src/lib/interfaces/access.interface.ts +++ b/libs/common/src/lib/interfaces/access.interface.ts @@ -2,10 +2,12 @@ import { AccessType } from '@ghostfolio/common/types'; import { AccessPermission } from '@prisma/client'; +import { AssetProfileIdentifier } from './asset-profile-identifier.interface'; + export interface AccessFilter { accountIds?: string[]; assetClasses?: string[]; - holdings?: { dataSource: string; symbol: string }[]; + holdings?: AssetProfileIdentifier[]; tagIds?: string[]; } From 08a4bce3703ad0e33a6156f29100e8d057649641 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Mart=C3=ADn?= Date: Fri, 31 Oct 2025 19:32:02 +0100 Subject: [PATCH 13/14] Feature: Remove comments in access dialog component for clarity --- .../create-or-update-access-dialog.component.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts index ec63c268a..50034fbb3 100644 --- a/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts +++ b/apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts @@ -210,7 +210,6 @@ export class GfCreateOrUpdateAccessDialogComponent private loadFilterData() { const existingFilter = this.data.access.settings?.filter; - // Cargar cuentas this.dataService .fetchAccounts() .pipe(takeUntil(this.unsubscribeSubject)) @@ -219,7 +218,6 @@ export class GfCreateOrUpdateAccessDialogComponent this.updateFiltersFormControl(existingFilter); }); - // Cargar holdings y asset classes this.dataService .fetchPortfolioDetails({}) .pipe(takeUntil(this.unsubscribeSubject)) @@ -227,7 +225,6 @@ export class GfCreateOrUpdateAccessDialogComponent if (response.holdings) { this.holdings = Object.values(response.holdings); - // Extraer asset classes únicas const assetClassesSet = new Set(); Object.values(response.holdings).forEach((holding) => { if (holding.assetClass) { @@ -245,7 +242,6 @@ export class GfCreateOrUpdateAccessDialogComponent this.changeDetectorRef.markForCheck(); }); - // Cargar tags this.dataService .fetchTags() .pipe(takeUntil(this.unsubscribeSubject)) @@ -308,7 +304,6 @@ export class GfCreateOrUpdateAccessDialogComponent } private async createAccess() { - // Construir el objeto filter si estamos en modo PUBLIC const filter = this.showFilterPanel ? this.buildFilterObject() : undefined; const access: CreateAccessDto = { @@ -350,7 +345,6 @@ export class GfCreateOrUpdateAccessDialogComponent } private async updateAccess() { - // Construir el objeto filter si estamos en modo PUBLIC const filter = this.showFilterPanel ? this.buildFilterObject() : undefined; const access: UpdateAccessDto = { From db27e5cefee54f2a7ddb777f38efae74cf760c7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Mart=C3=ADn?= Date: Fri, 31 Oct 2025 19:41:19 +0100 Subject: [PATCH 14/14] Feature: Reorganize create and delete access methods in AccessController for improved clarity --- apps/api/src/app/access/access.controller.ts | 118 +++++++++---------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/apps/api/src/app/access/access.controller.ts b/apps/api/src/app/access/access.controller.ts index cf1c2c1e4..60ec624da 100644 --- a/apps/api/src/app/access/access.controller.ts +++ b/apps/api/src/app/access/access.controller.ts @@ -34,65 +34,6 @@ export class AccessController { @Inject(REQUEST) private readonly request: RequestWithUser ) {} - @HasPermission(permissions.createAccess) - @Post() - @UseGuards(AuthGuard('jwt'), HasPermissionGuard) - public async createAccess( - @Body() data: CreateAccessDto - ): Promise { - if ( - this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && - this.request.user.subscription.type === 'Basic' - ) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - - try { - const settings: AccessSettings = data.filter - ? { filter: data.filter } - : {}; - - return this.accessService.createAccess({ - alias: data.alias || undefined, - granteeUser: data.granteeUserId - ? { connect: { id: data.granteeUserId } } - : undefined, - permissions: data.permissions, - settings: settings as Prisma.InputJsonValue, - user: { connect: { id: this.request.user.id } } - }); - } catch { - throw new HttpException( - getReasonPhrase(StatusCodes.BAD_REQUEST), - StatusCodes.BAD_REQUEST - ); - } - } - - @Delete(':id') - @HasPermission(permissions.deleteAccess) - @UseGuards(AuthGuard('jwt'), HasPermissionGuard) - public async deleteAccess(@Param('id') id: string): Promise { - const originalAccess = await this.accessService.access({ - id, - userId: this.request.user.id - }); - - if (!originalAccess) { - throw new HttpException( - getReasonPhrase(StatusCodes.FORBIDDEN), - StatusCodes.FORBIDDEN - ); - } - - return this.accessService.deleteAccess({ - id - }); - } - @Get() @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async getAllAccesses(): Promise { @@ -135,6 +76,44 @@ export class AccessController { ); } + @HasPermission(permissions.createAccess) + @Post() + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) + public async createAccess( + @Body() data: CreateAccessDto + ): Promise { + if ( + this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && + this.request.user.subscription.type === 'Basic' + ) { + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); + } + + try { + const settings: AccessSettings = data.filter + ? { filter: data.filter } + : {}; + + return this.accessService.createAccess({ + alias: data.alias || undefined, + granteeUser: data.granteeUserId + ? { connect: { id: data.granteeUserId } } + : undefined, + permissions: data.permissions, + settings: settings as Prisma.InputJsonValue, + user: { connect: { id: this.request.user.id } } + }); + } catch { + throw new HttpException( + getReasonPhrase(StatusCodes.BAD_REQUEST), + StatusCodes.BAD_REQUEST + ); + } + } + @HasPermission(permissions.updateAccess) @Put(':id') @UseGuards(AuthGuard('jwt'), HasPermissionGuard) @@ -187,4 +166,25 @@ export class AccessController { ); } } + + @Delete(':id') + @HasPermission(permissions.deleteAccess) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) + public async deleteAccess(@Param('id') id: string): Promise { + const originalAccess = await this.accessService.access({ + id, + userId: this.request.user.id + }); + + if (!originalAccess) { + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); + } + + return this.accessService.deleteAccess({ + id + }); + } }