From affea1563d226c7774a6f132359e6df96750ac5b Mon Sep 17 00:00:00 2001 From: KenTandrian Date: Thu, 14 May 2026 11:49:37 +0700 Subject: [PATCH 1/5] feat(client): resolve errors --- .../import-activities-dialog.component.ts | 47 +++++++++---------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts index 10df36857..76e68c3e4 100644 --- a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts @@ -26,7 +26,7 @@ import { } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { - FormBuilder, + FormControl, FormGroup, ReactiveFormsModule, Validators @@ -84,7 +84,6 @@ import { ImportActivitiesDialogParams } from './interfaces/interfaces'; export class GfImportActivitiesDialogComponent { public accounts: CreateAccountWithBalancesDto[] = []; public activities: Activity[] = []; - public assetProfileForm: FormGroup; public assetProfiles: CreateAssetProfileWithMarketDataDto[] = []; public dataSource: MatTableDataSource; public details: any[] = []; @@ -104,13 +103,18 @@ export class GfImportActivitiesDialogComponent { public tags: CreateTagDto[] = []; public totalItems: number; + protected readonly assetProfileForm = new FormGroup({ + assetProfileIdentifier: new FormControl(null, { + validators: [Validators.required] + }) + }); + public constructor( private changeDetectorRef: ChangeDetectorRef, @Inject(MAT_DIALOG_DATA) public data: ImportActivitiesDialogParams, private dataService: DataService, private destroyRef: DestroyRef, private deviceDetectorService: DeviceDetectorService, - private formBuilder: FormBuilder, public dialogRef: MatDialogRef, private importActivitiesService: ImportActivitiesService, private snackBar: MatSnackBar @@ -123,10 +127,6 @@ export class GfImportActivitiesDialogComponent { this.stepperOrientation = this.deviceType === 'mobile' ? 'vertical' : 'horizontal'; - this.assetProfileForm = this.formBuilder.group({ - assetProfileIdentifier: [undefined, Validators.required] - }); - if ( this.data?.activityTypes?.length === 1 && this.data?.activityTypes?.[0] === 'DIVIDEND' @@ -135,7 +135,7 @@ export class GfImportActivitiesDialogComponent { this.dialogTitle = $localize`Import Dividends`; this.mode = 'DIVIDEND'; - this.assetProfileForm.get('assetProfileIdentifier').disable(); + this.assetProfileForm.controls.assetProfileIdentifier.disable(); this.dataService .fetchPortfolioHoldings({ @@ -156,7 +156,7 @@ export class GfImportActivitiesDialogComponent { this.holdings = sortBy(holdings, ({ name }) => { return name.toLowerCase(); }); - this.assetProfileForm.get('assetProfileIdentifier').enable(); + this.assetProfileForm.controls.assetProfileIdentifier.enable(); this.isLoading = false; @@ -225,11 +225,14 @@ export class GfImportActivitiesDialogComponent { } public onLoadDividends(aStepper: MatStepper) { - this.assetProfileForm.get('assetProfileIdentifier').disable(); + this.assetProfileForm.controls.assetProfileIdentifier.disable(); - const { dataSource, symbol } = this.assetProfileForm.get( - 'assetProfileIdentifier' - ).value; + const { dataSource, symbol } = + this.assetProfileForm.controls.assetProfileIdentifier.value ?? {}; + + if (!dataSource || !symbol) { + return; + } this.dataService .fetchDividendsImport({ @@ -258,7 +261,7 @@ export class GfImportActivitiesDialogComponent { this.errorMessages = []; this.importStep = ImportStep.SELECT_ACTIVITIES; this.pageIndex = 0; - this.assetProfileForm.get('assetProfileIdentifier').enable(); + this.assetProfileForm.controls.assetProfileIdentifier.enable(); aStepper.reset(); } @@ -270,8 +273,10 @@ export class GfImportActivitiesDialogComponent { input.onchange = (event) => { // Getting the file reference - const file = (event.target as HTMLInputElement).files[0]; - this.handleFile({ file, stepper }); + const file = (event.target as HTMLInputElement).files?.[0]; + if (file) { + this.handleFile({ file, stepper }); + } }; input.click(); @@ -283,13 +288,7 @@ export class GfImportActivitiesDialogComponent { }); } - private async handleFile({ - file, - stepper - }: { - file: File; - stepper: MatStepper; - }): Promise { + private handleFile({ file, stepper }: { file: File; stepper: MatStepper }) { this.snackBar.open('⏳ ' + $localize`Validating data...`); // Setting up the reader @@ -297,7 +296,7 @@ export class GfImportActivitiesDialogComponent { reader.readAsText(file, 'UTF-8'); reader.onload = async (readerEvent) => { - const fileContent = readerEvent.target.result as string; + const fileContent = readerEvent.target?.result as string; const fileExtension = file.name.split('.').pop()?.toLowerCase(); try { From 4b3ce969906c5d56a9ab716e5ef629188bc4178e Mon Sep 17 00:00:00 2001 From: KenTandrian Date: Thu, 14 May 2026 11:52:55 +0700 Subject: [PATCH 2/5] feat(client): enforce encapsulation --- .../import-activities-dialog.component.ts | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts index 76e68c3e4..49afbf893 100644 --- a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts @@ -82,40 +82,40 @@ import { ImportActivitiesDialogParams } from './interfaces/interfaces'; templateUrl: 'import-activities-dialog.html' }) export class GfImportActivitiesDialogComponent { - public accounts: CreateAccountWithBalancesDto[] = []; - public activities: Activity[] = []; - public assetProfiles: CreateAssetProfileWithMarketDataDto[] = []; - public dataSource: MatTableDataSource; - public details: any[] = []; - public deviceType: string; - public dialogTitle = $localize`Import Activities`; - public errorMessages: string[] = []; - public holdings: PortfolioPosition[] = []; - public importStep: ImportStep = ImportStep.UPLOAD_FILE; - public isLoading = false; - public mode: 'DIVIDEND'; - public pageIndex = 0; - public pageSize = 8; - public selectedActivities: Activity[] = []; - public sortColumn = 'date'; - public sortDirection: SortDirection = 'desc'; - public stepperOrientation: StepperOrientation; - public tags: CreateTagDto[] = []; - public totalItems: number; - protected readonly assetProfileForm = new FormGroup({ assetProfileIdentifier: new FormControl(null, { validators: [Validators.required] }) }); + protected dataSource: MatTableDataSource; + protected details: any[] = []; + protected dialogTitle = $localize`Import Activities`; + protected errorMessages: string[] = []; + protected holdings: PortfolioPosition[] = []; + protected importStep: ImportStep = ImportStep.UPLOAD_FILE; + protected isLoading = false; + protected mode: 'DIVIDEND'; + protected pageIndex = 0; + protected pageSize = 8; + protected selectedActivities: Activity[] = []; + protected sortColumn = 'date'; + protected sortDirection: SortDirection = 'desc'; + protected stepperOrientation: StepperOrientation; + protected totalItems: number; + + private accounts: CreateAccountWithBalancesDto[] = []; + private activities: Activity[] = []; + private assetProfiles: CreateAssetProfileWithMarketDataDto[] = []; + private deviceType: string; + private tags: CreateTagDto[] = []; public constructor( private changeDetectorRef: ChangeDetectorRef, - @Inject(MAT_DIALOG_DATA) public data: ImportActivitiesDialogParams, + @Inject(MAT_DIALOG_DATA) protected data: ImportActivitiesDialogParams, private dataService: DataService, private destroyRef: DestroyRef, private deviceDetectorService: DeviceDetectorService, - public dialogRef: MatDialogRef, + private dialogRef: MatDialogRef, private importActivitiesService: ImportActivitiesService, private snackBar: MatSnackBar ) { @@ -165,11 +165,11 @@ export class GfImportActivitiesDialogComponent { } } - public onCancel() { + protected onCancel() { this.dialogRef.close(); } - public async onImportActivities() { + protected async onImportActivities() { try { this.snackBar.open('⏳ ' + $localize`Importing data...`); @@ -202,7 +202,7 @@ export class GfImportActivitiesDialogComponent { } } - public onFilesDropped({ + protected onFilesDropped({ files, stepper }: { @@ -216,7 +216,7 @@ export class GfImportActivitiesDialogComponent { this.handleFile({ stepper, file: files[0] }); } - public onImportStepChange(event: StepperSelectionEvent) { + protected onImportStepChange(event: StepperSelectionEvent) { if (event.selectedIndex === ImportStep.UPLOAD_FILE) { this.importStep = ImportStep.UPLOAD_FILE; } else if (event.selectedIndex === ImportStep.SELECT_ACTIVITIES) { @@ -224,7 +224,7 @@ export class GfImportActivitiesDialogComponent { } } - public onLoadDividends(aStepper: MatStepper) { + protected onLoadDividends(aStepper: MatStepper) { this.assetProfileForm.controls.assetProfileIdentifier.disable(); const { dataSource, symbol } = @@ -252,11 +252,11 @@ export class GfImportActivitiesDialogComponent { }); } - public onPageChanged({ pageIndex }: PageEvent) { + protected onPageChanged({ pageIndex }: PageEvent) { this.pageIndex = pageIndex; } - public onReset(aStepper: MatStepper) { + protected onReset(aStepper: MatStepper) { this.details = []; this.errorMessages = []; this.importStep = ImportStep.SELECT_ACTIVITIES; @@ -266,7 +266,7 @@ export class GfImportActivitiesDialogComponent { aStepper.reset(); } - public onSelectFile(stepper: MatStepper) { + protected onSelectFile(stepper: MatStepper) { const input = document.createElement('input'); input.accept = 'application/JSON, .csv'; input.type = 'file'; @@ -282,7 +282,7 @@ export class GfImportActivitiesDialogComponent { input.click(); } - public updateSelection(activities: Activity[]) { + protected updateSelection(activities: Activity[]) { this.selectedActivities = activities.filter(({ error }) => { return !error; }); From c4399c172c3270d2cfcd61d82c030fe75b235670 Mon Sep 17 00:00:00 2001 From: KenTandrian Date: Thu, 14 May 2026 11:55:06 +0700 Subject: [PATCH 3/5] feat(client): enforce immutability --- .../import-activities-dialog.component.ts | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts index 49afbf893..f93350148 100644 --- a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts @@ -96,10 +96,10 @@ export class GfImportActivitiesDialogComponent { protected isLoading = false; protected mode: 'DIVIDEND'; protected pageIndex = 0; - protected pageSize = 8; + protected readonly pageSize = 8; protected selectedActivities: Activity[] = []; - protected sortColumn = 'date'; - protected sortDirection: SortDirection = 'desc'; + protected readonly sortColumn = 'date'; + protected readonly sortDirection: SortDirection = 'desc'; protected stepperOrientation: StepperOrientation; protected totalItems: number; @@ -110,14 +110,15 @@ export class GfImportActivitiesDialogComponent { private tags: CreateTagDto[] = []; public constructor( - private changeDetectorRef: ChangeDetectorRef, - @Inject(MAT_DIALOG_DATA) protected data: ImportActivitiesDialogParams, - private dataService: DataService, - private destroyRef: DestroyRef, - private deviceDetectorService: DeviceDetectorService, - private dialogRef: MatDialogRef, - private importActivitiesService: ImportActivitiesService, - private snackBar: MatSnackBar + private readonly changeDetectorRef: ChangeDetectorRef, + @Inject(MAT_DIALOG_DATA) + protected readonly data: ImportActivitiesDialogParams, + private readonly dataService: DataService, + private readonly destroyRef: DestroyRef, + private readonly deviceDetectorService: DeviceDetectorService, + private readonly dialogRef: MatDialogRef, + private readonly importActivitiesService: ImportActivitiesService, + private readonly snackBar: MatSnackBar ) { addIcons({ cloudUploadOutline, warningOutline }); } From 2a54227feffd00720d6e220a932e95deeea347f0 Mon Sep 17 00:00:00 2001 From: KenTandrian Date: Thu, 14 May 2026 12:04:25 +0700 Subject: [PATCH 4/5] feat(client): replace constructor based DI with inject function --- .../import-activities-dialog.component.ts | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts index f93350148..798da16d2 100644 --- a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts @@ -22,7 +22,7 @@ import { ChangeDetectorRef, Component, DestroyRef, - Inject + inject } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { @@ -87,6 +87,8 @@ export class GfImportActivitiesDialogComponent { validators: [Validators.required] }) }); + protected readonly data = + inject(MAT_DIALOG_DATA); protected dataSource: MatTableDataSource; protected details: any[] = []; protected dialogTitle = $localize`Import Activities`; @@ -109,17 +111,16 @@ export class GfImportActivitiesDialogComponent { private deviceType: string; private tags: CreateTagDto[] = []; - public constructor( - private readonly changeDetectorRef: ChangeDetectorRef, - @Inject(MAT_DIALOG_DATA) - protected readonly data: ImportActivitiesDialogParams, - private readonly dataService: DataService, - private readonly destroyRef: DestroyRef, - private readonly deviceDetectorService: DeviceDetectorService, - private readonly dialogRef: MatDialogRef, - private readonly importActivitiesService: ImportActivitiesService, - private readonly snackBar: MatSnackBar - ) { + private readonly changeDetectorRef = inject(ChangeDetectorRef); + private readonly dataService = inject(DataService); + private readonly destroyRef = inject(DestroyRef); + private readonly deviceDetectorService = inject(DeviceDetectorService); + private readonly dialogRef = + inject>(MatDialogRef); + private readonly importActivitiesService = inject(ImportActivitiesService); + private readonly snackBar = inject(MatSnackBar); + + public constructor() { addIcons({ cloudUploadOutline, warningOutline }); } From 8c33f5153628c2d6d35bea2ba902a545e2bf42fe Mon Sep 17 00:00:00 2001 From: KenTandrian Date: Thu, 14 May 2026 12:22:01 +0700 Subject: [PATCH 5/5] fix(client): remove device detector service --- .../import-activities-dialog.component.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts index 798da16d2..23d0d3959 100644 --- a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts @@ -52,7 +52,6 @@ import { addIcons } from 'ionicons'; import { cloudUploadOutline, warningOutline } from 'ionicons/icons'; import { isArray, sortBy } from 'lodash'; import ms from 'ms'; -import { DeviceDetectorService } from 'ngx-device-detector'; import { ImportStep } from './enums/import-step'; import { ImportActivitiesDialogParams } from './interfaces/interfaces'; @@ -102,19 +101,18 @@ export class GfImportActivitiesDialogComponent { protected selectedActivities: Activity[] = []; protected readonly sortColumn = 'date'; protected readonly sortDirection: SortDirection = 'desc'; - protected stepperOrientation: StepperOrientation; + protected readonly stepperOrientation: StepperOrientation = + this.data.deviceType === 'mobile' ? 'vertical' : 'horizontal'; protected totalItems: number; private accounts: CreateAccountWithBalancesDto[] = []; private activities: Activity[] = []; private assetProfiles: CreateAssetProfileWithMarketDataDto[] = []; - private deviceType: string; private tags: CreateTagDto[] = []; private readonly changeDetectorRef = inject(ChangeDetectorRef); private readonly dataService = inject(DataService); private readonly destroyRef = inject(DestroyRef); - private readonly deviceDetectorService = inject(DeviceDetectorService); private readonly dialogRef = inject>(MatDialogRef); private readonly importActivitiesService = inject(ImportActivitiesService); @@ -125,10 +123,6 @@ export class GfImportActivitiesDialogComponent { } public ngOnInit() { - this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType; - this.stepperOrientation = - this.deviceType === 'mobile' ? 'vertical' : 'horizontal'; - if ( this.data?.activityTypes?.length === 1 && this.data?.activityTypes?.[0] === 'DIVIDEND'