diff --git a/apps/api/src/app/access/access-filter.dto.ts b/apps/api/src/app/access/access-filter.dto.ts index ac8c81bb3..427627063 100644 --- a/apps/api/src/app/access/access-filter.dto.ts +++ b/apps/api/src/app/access/access-filter.dto.ts @@ -8,7 +8,7 @@ import { ValidateNested } from 'class-validator'; -export class HoldingFilterDto { +class HoldingFilterDto { @IsEnum(DataSource) dataSource: DataSource; @@ -29,8 +29,8 @@ export class AccessFilterDto { @IsArray() @IsOptional() - @ValidateNested({ each: true }) @Type(() => HoldingFilterDto) + @ValidateNested({ each: true }) holdings?: HoldingFilterDto[]; @IsArray() diff --git a/apps/api/src/app/access/access-settings.interface.ts b/apps/api/src/app/access/access-settings.interface.ts deleted file mode 100644 index 8e173fdae..000000000 --- a/apps/api/src/app/access/access-settings.interface.ts +++ /dev/null @@ -1 +0,0 @@ -export type { AccessSettings } from '@ghostfolio/common/interfaces'; diff --git a/apps/api/src/app/access/access.controller.ts b/apps/api/src/app/access/access.controller.ts index 791ea7d26..e114e14f3 100644 --- a/apps/api/src/app/access/access.controller.ts +++ b/apps/api/src/app/access/access.controller.ts @@ -19,14 +19,24 @@ import { } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; -import { Access as AccessModel } from '@prisma/client'; +import { Access as AccessModel, Prisma } from '@prisma/client'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; -import { AccessSettings } from './access-settings.interface'; 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( @@ -62,7 +72,7 @@ export class AccessController { ? { connect: { id: data.granteeUserId } } : undefined, permissions: data.permissions, - settings: settings as any, + settings: settings as Prisma.InputJsonValue, user: { connect: { id: this.request.user.id } } }); } catch { @@ -116,20 +126,20 @@ export class AccessController { if (granteeUser) { return { alias, + grantee: granteeUser?.id, id, permissions: accessPermissions, settings: settings as AccessSettings, - grantee: granteeUser?.id, type: 'PRIVATE' }; } return { alias, + grantee: 'Public', id, permissions: accessPermissions, settings: settings as AccessSettings, - grantee: 'Public', type: 'PUBLIC' }; } @@ -177,7 +187,7 @@ export class AccessController { ? { connect: { id: data.granteeUserId } } : { disconnect: true }, permissions: data.permissions, - settings: settings as any + settings: settings as Prisma.InputJsonValue }, where: { id } }); diff --git a/apps/api/src/app/endpoints/public/public.controller.ts b/apps/api/src/app/endpoints/public/public.controller.ts index 8a59ffc3a..f8622ebbc 100644 --- a/apps/api/src/app/endpoints/public/public.controller.ts +++ b/apps/api/src/app/endpoints/public/public.controller.ts @@ -74,7 +74,7 @@ export class PublicController { if (accessFilter) { // Add account filters - if (accessFilter.accountIds && accessFilter.accountIds.length > 0) { + if (accessFilter.accountIds?.length > 0) { portfolioFilters.push( ...accessFilter.accountIds.map((accountId) => ({ id: accountId, @@ -84,7 +84,7 @@ export class PublicController { } // Add asset class filters - if (accessFilter.assetClasses && accessFilter.assetClasses.length > 0) { + if (accessFilter.assetClasses?.length > 0) { portfolioFilters.push( ...accessFilter.assetClasses.map((assetClass) => ({ id: assetClass, @@ -94,7 +94,7 @@ export class PublicController { } // Add tag filters - if (accessFilter.tagIds && accessFilter.tagIds.length > 0) { + if (accessFilter.tagIds?.length > 0) { portfolioFilters.push( ...accessFilter.tagIds.map((tagId) => ({ id: tagId, @@ -105,7 +105,7 @@ export class PublicController { // Add holding filters (symbol + dataSource) // Each holding needs both DATA_SOURCE and SYMBOL filters - if (accessFilter.holdings && accessFilter.holdings.length > 0) { + if (accessFilter.holdings?.length > 0) { accessFilter.holdings.forEach((holding) => { portfolioFilters.push( { @@ -143,7 +143,6 @@ export class PublicController { }) ]); - // Filter out only the base currency cash holdings const baseCurrency = user.settings?.settings.baseCurrency ?? DEFAULT_CURRENCY; const filteredHoldings = Object.fromEntries( @@ -193,7 +192,6 @@ export class PublicController { withExcludedAccountsAndActivities: false }); - // If multiple holdings, filter activities manually let filteredActivities = activities; if (hasMultipleHoldingFilters && accessFilter.holdings) { filteredActivities = activities.filter((activity) => { 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 a099504a4..e6c14b623 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 @@ -2,6 +2,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 { AccountWithPlatform } from '@ghostfolio/common/types'; +import { + GfPortfolioFilterFormComponent, + PortfolioFilterFormValue +} from '@ghostfolio/ui/portfolio-filter-form'; import { ChangeDetectionStrategy, @@ -42,6 +46,7 @@ import { CreateOrUpdateAccessDialogParams } from './interfaces/interfaces'; host: { class: 'h-100' }, imports: [ FormsModule, + GfPortfolioFilterFormComponent, MatButtonModule, MatDialogModule, MatFormFieldModule, @@ -60,7 +65,6 @@ export class GfCreateOrUpdateAccessDialogComponent public mode: 'create' | 'update'; public showFilterPanel = false; - // Datos para el filtro public accounts: AccountWithPlatform[] = []; public assetClasses: Filter[] = []; public holdings: PortfolioPosition[] = []; @@ -69,12 +73,12 @@ export class GfCreateOrUpdateAccessDialogComponent private unsubscribeSubject = new Subject(); public constructor( - public dialogRef: MatDialogRef, private changeDetectorRef: ChangeDetectorRef, @Inject(MAT_DIALOG_DATA) private data: CreateOrUpdateAccessDialogParams, private dataService: DataService, private formBuilder: FormBuilder, - private notificationService: NotificationService + private notificationService: NotificationService, + public dialogRef: MatDialogRef ) { this.mode = this.data.access?.id ? 'update' : 'create'; } @@ -84,10 +88,7 @@ export class GfCreateOrUpdateAccessDialogComponent this.accessForm = this.formBuilder.group({ alias: [this.data.access.alias], - filterAccount: [null], - filterAssetClass: [null], - filterHolding: [null], - filterTag: [null], + filters: [null], granteeUserId: [ this.data.access.grantee, isPublic @@ -113,11 +114,7 @@ export class GfCreateOrUpdateAccessDialogComponent (control: AbstractControl) => Validators.required(control) ]); this.showFilterPanel = false; - // Limpiar los filtros - this.accessForm.get('filterAccount')?.setValue(null); - this.accessForm.get('filterAssetClass')?.setValue(null); - this.accessForm.get('filterHolding')?.setValue(null); - this.accessForm.get('filterTag')?.setValue(null); + this.accessForm.get('filters')?.setValue(null); } else { granteeUserIdControl.clearValidators(); granteeUserIdControl.setValue(null); @@ -131,7 +128,6 @@ export class GfCreateOrUpdateAccessDialogComponent this.changeDetectorRef.markForCheck(); }); - // Si ya es público al iniciar, mostrar el panel y cargar datos if (isPublic) { this.showFilterPanel = true; this.loadFilterData(); @@ -163,19 +159,16 @@ export class GfCreateOrUpdateAccessDialogComponent tagIds?: string[]; } | undefined { - const filterAccount = this.accessForm.get('filterAccount')?.value as - | string - | null; - const filterAssetClass = this.accessForm.get('filterAssetClass')?.value as - | string - | null; - const filterHolding = this.accessForm.get('filterHolding')?.value as - | string - | null; - const filterTag = this.accessForm.get('filterTag')?.value as string | null; - - // Solo retornar filtro si hay al menos un campo con valor - if (!filterAccount && !filterAssetClass && !filterHolding && !filterTag) { + const filterValue = this.accessForm.get('filters') + ?.value as PortfolioFilterFormValue | null; + + if ( + !filterValue || + (!filterValue.account && + !filterValue.assetClass && + !filterValue.holding && + !filterValue.tag) + ) { return undefined; } @@ -186,29 +179,25 @@ export class GfCreateOrUpdateAccessDialogComponent tagIds?: string[]; } = {}; - if (filterAccount) { - filter.accountIds = [filterAccount]; + if (filterValue.account) { + filter.accountIds = [filterValue.account]; } - if (filterAssetClass) { - filter.assetClasses = [filterAssetClass]; + if (filterValue.assetClass) { + filter.assetClasses = [filterValue.assetClass]; } - if (filterHolding) { - // Buscar el holding seleccionado para obtener dataSource y symbol - const holding = this.holdings.find((h) => h.symbol === filterHolding); - if (holding) { - filter.holdings = [ - { - dataSource: holding.dataSource, - symbol: holding.symbol - } - ]; - } + if (filterValue.holding) { + filter.holdings = [ + { + dataSource: filterValue.holding.dataSource, + symbol: filterValue.holding.symbol + } + ]; } - if (filterTag) { - filter.tagIds = [filterTag]; + if (filterValue.tag) { + filter.tagIds = [filterValue.tag]; } return filter; @@ -223,15 +212,7 @@ export class GfCreateOrUpdateAccessDialogComponent .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((response) => { this.accounts = response.accounts; - - // Si existe un filtro de cuenta, establecerlo - if (existingFilter?.accountIds?.[0]) { - this.accessForm - .get('filterAccount') - ?.setValue(existingFilter.accountIds[0]); - } - - this.changeDetectorRef.markForCheck(); + this.updateFiltersFormControl(existingFilter); }); // Cargar holdings y asset classes @@ -255,19 +236,7 @@ export class GfCreateOrUpdateAccessDialogComponent type: 'ASSET_CLASS' as const })); - // Si existe un filtro de asset class, establecerlo - if (existingFilter?.assetClasses?.[0]) { - this.accessForm - .get('filterAssetClass') - ?.setValue(existingFilter.assetClasses[0]); - } - - // Si existe un filtro de holding, establecerlo - if (existingFilter?.holdings?.[0]?.symbol) { - this.accessForm - .get('filterHolding') - ?.setValue(existingFilter.holdings[0].symbol); - } + this.updateFiltersFormControl(existingFilter); } this.changeDetectorRef.markForCheck(); }); @@ -283,15 +252,57 @@ export class GfCreateOrUpdateAccessDialogComponent type: 'TAG' as const })); - // Si existe un filtro de tag, establecerlo - if (existingFilter?.tagIds?.[0]) { - this.accessForm.get('filterTag')?.setValue(existingFilter.tagIds[0]); - } - + this.updateFiltersFormControl(existingFilter); this.changeDetectorRef.markForCheck(); }); } + private updateFiltersFormControl( + existingFilter: + | { + accountIds?: string[]; + assetClasses?: string[]; + holdings?: { dataSource: string; symbol: string }[]; + tagIds?: string[]; + } + | undefined + ) { + if (!existingFilter) { + return; + } + + const filterValue: Partial = {}; + + if (existingFilter.accountIds?.[0] && this.accounts.length > 0) { + filterValue.account = existingFilter.accountIds[0]; + } + + if (existingFilter.assetClasses?.[0] && this.assetClasses.length > 0) { + filterValue.assetClass = existingFilter.assetClasses[0]; + } + + if (existingFilter.holdings?.[0] && this.holdings.length > 0) { + const holdingData = existingFilter.holdings[0]; + const holding = this.holdings.find( + (h) => + h.dataSource === holdingData.dataSource && + h.symbol === holdingData.symbol + ); + if (holding) { + filterValue.holding = holding; + } + } + + if (existingFilter.tagIds?.[0] && this.tags.length > 0) { + filterValue.tag = existingFilter.tagIds[0]; + } + + if (Object.keys(filterValue).length > 0) { + this.accessForm.get('filters')?.setValue(filterValue); + this.changeDetectorRef.markForCheck(); + } + } + private async createAccess() { // Construir el objeto filter si estamos en modo PUBLIC const filter = this.showFilterPanel ? this.buildFilterObject() : undefined; 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 42001c3b4..0e5fe097b 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 @@ -62,75 +62,19 @@ @if (showFilterPanel) {
-

- Filter Settings (Optional) -

-

+

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; + } }