|
|
|
@ -1,8 +1,7 @@ |
|
|
|
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 { |
|
|
|
ChangeDetectionStrategy, |
|
|
|
@ -13,6 +12,7 @@ import { |
|
|
|
OnInit |
|
|
|
} from '@angular/core'; |
|
|
|
import { |
|
|
|
AbstractControl, |
|
|
|
FormBuilder, |
|
|
|
FormGroup, |
|
|
|
FormsModule, |
|
|
|
@ -28,9 +28,13 @@ import { |
|
|
|
import { MatFormFieldModule } from '@angular/material/form-field'; |
|
|
|
import { MatInputModule } from '@angular/material/input'; |
|
|
|
import { MatSelectModule } from '@angular/material/select'; |
|
|
|
import { AccessPermission } from '@prisma/client'; |
|
|
|
import { StatusCodes } from 'http-status-codes'; |
|
|
|
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({ |
|
|
|
@ -54,13 +58,20 @@ export class GfCreateOrUpdateAccessDialogComponent |
|
|
|
{ |
|
|
|
public accessForm: FormGroup; |
|
|
|
public mode: 'create' | 'update'; |
|
|
|
public showFilterPanel = false; |
|
|
|
|
|
|
|
// Datos para el filtro
|
|
|
|
public accounts: AccountWithPlatform[] = []; |
|
|
|
public assetClasses: Filter[] = []; |
|
|
|
public holdings: PortfolioPosition[] = []; |
|
|
|
public tags: Filter[] = []; |
|
|
|
|
|
|
|
private unsubscribeSubject = new Subject<void>(); |
|
|
|
|
|
|
|
public constructor( |
|
|
|
public dialogRef: MatDialogRef<GfCreateOrUpdateAccessDialogComponent>, |
|
|
|
private changeDetectorRef: ChangeDetectorRef, |
|
|
|
@Inject(MAT_DIALOG_DATA) private data: CreateOrUpdateAccessDialogParams, |
|
|
|
public dialogRef: MatDialogRef<GfCreateOrUpdateAccessDialogComponent>, |
|
|
|
private dataService: DataService, |
|
|
|
private formBuilder: FormBuilder, |
|
|
|
private notificationService: NotificationService |
|
|
|
@ -73,14 +84,23 @@ export class GfCreateOrUpdateAccessDialogComponent |
|
|
|
|
|
|
|
this.accessForm = this.formBuilder.group({ |
|
|
|
alias: [this.data.access.alias], |
|
|
|
filterAccount: [null], |
|
|
|
filterAssetClass: [null], |
|
|
|
filterHolding: [null], |
|
|
|
filterTag: [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)] |
|
|
|
] |
|
|
|
}); |
|
|
|
|
|
|
|
@ -89,17 +109,33 @@ export class GfCreateOrUpdateAccessDialogComponent |
|
|
|
const permissionsControl = this.accessForm.get('permissions'); |
|
|
|
|
|
|
|
if (accessType === 'PRIVATE') { |
|
|
|
granteeUserIdControl.setValidators(Validators.required); |
|
|
|
granteeUserIdControl.setValidators([ |
|
|
|
(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); |
|
|
|
} 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,11 +155,154 @@ export class GfCreateOrUpdateAccessDialogComponent |
|
|
|
this.unsubscribeSubject.complete(); |
|
|
|
} |
|
|
|
|
|
|
|
private buildFilterObject(): |
|
|
|
| { |
|
|
|
accountIds?: string[]; |
|
|
|
assetClasses?: string[]; |
|
|
|
holdings?: { dataSource: string; symbol: string }[]; |
|
|
|
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) { |
|
|
|
return undefined; |
|
|
|
} |
|
|
|
|
|
|
|
const filter: { |
|
|
|
accountIds?: string[]; |
|
|
|
assetClasses?: string[]; |
|
|
|
holdings?: { dataSource: string; symbol: string }[]; |
|
|
|
tagIds?: string[]; |
|
|
|
} = {}; |
|
|
|
|
|
|
|
if (filterAccount) { |
|
|
|
filter.accountIds = [filterAccount]; |
|
|
|
} |
|
|
|
|
|
|
|
if (filterAssetClass) { |
|
|
|
filter.assetClasses = [filterAssetClass]; |
|
|
|
} |
|
|
|
|
|
|
|
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 (filterTag) { |
|
|
|
filter.tagIds = [filterTag]; |
|
|
|
} |
|
|
|
|
|
|
|
return filter; |
|
|
|
} |
|
|
|
|
|
|
|
private loadFilterData() { |
|
|
|
const existingFilter = this.data.access.settings?.filter; |
|
|
|
|
|
|
|
// Cargar cuentas
|
|
|
|
this.dataService |
|
|
|
.fetchAccounts() |
|
|
|
.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(); |
|
|
|
}); |
|
|
|
|
|
|
|
// 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<string>(); |
|
|
|
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 |
|
|
|
})); |
|
|
|
|
|
|
|
// 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.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 |
|
|
|
})); |
|
|
|
|
|
|
|
// Si existe un filtro de tag, establecerlo
|
|
|
|
if (existingFilter?.tagIds?.[0]) { |
|
|
|
this.accessForm.get('filterTag')?.setValue(existingFilter.tagIds[0]); |
|
|
|
} |
|
|
|
|
|
|
|
this.changeDetectorRef.markForCheck(); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
private async createAccess() { |
|
|
|
// Construir el objeto filter si estamos en modo PUBLIC
|
|
|
|
const filter = this.showFilterPanel ? this.buildFilterObject() : undefined; |
|
|
|
|
|
|
|
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, |
|
|
|
filter: filter, |
|
|
|
granteeUserId: this.accessForm.get('granteeUserId')?.value as string, |
|
|
|
permissions: [ |
|
|
|
this.accessForm.get('permissions')?.value as AccessPermission |
|
|
|
] |
|
|
|
}; |
|
|
|
|
|
|
|
try { |
|
|
|
@ -136,8 +315,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 as number)) { |
|
|
|
this.notificationService.alert({ |
|
|
|
title: $localize`Oops! Could not grant access.` |
|
|
|
}); |
|
|
|
@ -156,11 +335,17 @@ 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 = { |
|
|
|
alias: this.accessForm.get('alias').value, |
|
|
|
granteeUserId: this.accessForm.get('granteeUserId').value, |
|
|
|
alias: this.accessForm.get('alias')?.value as string, |
|
|
|
filter: filter, |
|
|
|
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 { |
|
|
|
@ -173,8 +358,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 as number)) { |
|
|
|
this.notificationService.alert({ |
|
|
|
title: $localize`Oops! Could not update access.` |
|
|
|
}); |
|
|
|
|