@ -1,5 +1,5 @@
import { GfFileDropDirective } from '@ghostfolio/client/directives/file-drop/file-drop.directive' ;
import { ImportActivitiesService } from '@ghostfolio/client/services/import-activities.service' ;
import { UserService } from '@ghostfolio/client/services/user/user.service' ;
import {
AI_ACTIVITIES_FILE_MAXIMUM_SIZE ,
AI_MAX_ACTIVITIES ,
@ -22,6 +22,10 @@ import { AssetClass } from '@ghostfolio/prisma/enums';
import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table' ;
import { GfDialogFooterComponent } from '@ghostfolio/ui/dialog-footer' ;
import { GfDialogHeaderComponent } from '@ghostfolio/ui/dialog-header' ;
import {
FileFormat ,
GfFileDropAreaComponent
} from '@ghostfolio/ui/file-drop-area' ;
import { DataService } from '@ghostfolio/ui/services' ;
import {
@ -66,17 +70,16 @@ import { MatTableDataSource } from '@angular/material/table';
import { MatTooltipModule } from '@angular/material/tooltip' ;
import { IonIcon } from '@ionic/angular/standalone' ;
import { addIcons } from 'ionicons' ;
import {
cloudUploadOutline ,
informationCircleOutline ,
warningOutline
} from 'ionicons/icons' ;
import { informationCircleOutline , warningOutline } from 'ionicons/icons' ;
import { isArray , sortBy } from 'lodash-es' ;
import ms from 'ms' ;
import { from , switchMap } from 'rxjs' ;
import { ImportStep } from './enums/import-step' ;
import { ImportActivitiesDialogParams } from './interfaces/interfaces' ;
import {
ImportActivitiesDialogParams ,
ImportActivitiesDialogResult
} from './interfaces/interfaces' ;
@Component ( {
changeDetection : ChangeDetectionStrategy.OnPush ,
@ -86,7 +89,7 @@ import { ImportActivitiesDialogParams } from './interfaces/interfaces';
GfActivitiesTableComponent ,
GfDialogFooterComponent ,
GfDialogHeaderComponent ,
GfFileDropDirective ,
GfFileDropAreaComponent ,
IonIcon ,
MatButtonModule ,
MatCheckboxModule ,
@ -104,14 +107,49 @@ import { ImportActivitiesDialogParams } from './interfaces/interfaces';
templateUrl : 'import-activities-dialog.html'
} )
export class GfImportActivitiesDialogComponent {
private static readonly FILE_EXTENSIONS_OF_GHOSTFOLIO_AI = [
'csv' ,
'jpeg' ,
'jpg' ,
'pdf' ,
'png'
private static readonly FILE_FORMAT_CSV : FileFormat = {
extensions : [ 'csv' ] ,
name : 'CSV' ,
sampleUrl :
'https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok/sample.csv'
} ;
private static readonly FILE_FORMAT_JSON : FileFormat = {
extensions : [ 'json' ] ,
name : 'JSON' ,
sampleUrl :
'https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok/sample.json'
} ;
private static readonly FILE_FORMATS : FileFormat [ ] = [
GfImportActivitiesDialogComponent . FILE_FORMAT_CSV ,
GfImportActivitiesDialogComponent . FILE_FORMAT_JSON
] ;
private static readonly FILE_FORMATS_OF_GHOSTFOLIO_AI : FileFormat [ ] = [
GfImportActivitiesDialogComponent . FILE_FORMAT_CSV ,
{ extensions : [ 'jpeg' , 'jpg' ] , name : 'JPG' } ,
{ extensions : [ 'pdf' ] , name : 'PDF' } ,
{ extensions : [ 'png' ] , name : 'PNG' }
] ;
private static readonly FILE_EXTENSIONS_OF_GHOSTFOLIO_AI =
GfImportActivitiesDialogComponent . FILE_FORMATS_OF_GHOSTFOLIO_AI . flatMap (
( { extensions } ) = > {
return extensions ;
}
) ;
private static readonly FILE_FORMATS_WITH_GHOSTFOLIO_AI = sortBy (
[
. . . GfImportActivitiesDialogComponent . FILE_FORMATS_OF_GHOSTFOLIO_AI ,
GfImportActivitiesDialogComponent . FILE_FORMAT_JSON
] ,
( { name } ) = > {
return name ;
}
) ;
protected readonly assetProfileForm = new FormGroup ( {
assetProfileIdentifier : new FormControl < PortfolioPosition | null > ( null , {
validators : [ Validators . required ]
@ -123,6 +161,7 @@ export class GfImportActivitiesDialogComponent {
protected details : any [ ] = [ ] ;
protected dialogTitle = $localize ` Import Activities ` ;
protected errorMessages : string [ ] = [ ] ;
protected readonly ghostfolioAiLoadingText = $localize ` Ghostfolio AI is processing the file... ` ;
protected readonly ghostfolioAiTooltip = $localize ` The content of your file will be processed by third-party AI providers. Please remove any personal data before uploading. ` ;
protected readonly hasPermissionToEnableGhostfolioAi = hasPermission (
@ -140,6 +179,7 @@ export class GfImportActivitiesDialogComponent {
protected isLoading = false ;
protected isMaxActivitiesOfGhostfolioAiReached = false ;
protected readonly language = document . documentElement . lang ;
protected readonly maxActivitiesOfGhostfolioAi = AI_MAX_ACTIVITIES ;
protected mode : 'DIVIDEND' ;
protected pageIndex = 0 ;
@ -155,6 +195,7 @@ export class GfImportActivitiesDialogComponent {
private accounts : CreateAccountWithBalancesDto [ ] = [ ] ;
private activities : Activity [ ] = [ ] ;
private assetProfiles : CreateAssetProfileWithMarketDataDto [ ] = [ ] ;
private hasChangedUserSettings = false ;
private platforms : CreatePlatformDto [ ] = [ ] ;
private tags : CreateTagDto [ ] = [ ] ;
@ -162,15 +203,35 @@ export class GfImportActivitiesDialogComponent {
private readonly dataService = inject ( DataService ) ;
private readonly destroyRef = inject ( DestroyRef ) ;
private readonly dialogRef =
inject < MatDialogRef < GfImportActivitiesDialogComponent > > ( MatDialogRef ) ;
inject <
MatDialogRef <
GfImportActivitiesDialogComponent ,
ImportActivitiesDialogResult
>
> ( MatDialogRef ) ;
private readonly importActivitiesService = inject ( ImportActivitiesService ) ;
private readonly snackBar = inject ( MatSnackBar ) ;
private readonly userService = inject ( UserService ) ;
public constructor ( ) {
addIcons ( { cloudUploadOutline , informationCircleOutline , warningOutline } ) ;
addIcons ( { informationCircleOutline , warningOutline } ) ;
}
protected get fileFormats() {
return this . isGhostfolioAiEnabled
? GfImportActivitiesDialogComponent . FILE_FORMATS_WITH_GHOSTFOLIO_AI
: GfImportActivitiesDialogComponent . FILE_FORMATS ;
}
public ngOnInit() {
// No takeUntilDestroyed(), the component is already destroyed here
// The opener refreshes the user if withRefresh is true
this . dialogRef . afterClosed ( ) . subscribe ( ( result ) = > {
if ( this . hasChangedUserSettings && ! result ? . withRefresh ) {
this . userService . get ( true ) . subscribe ( ) ;
}
} ) ;
if (
this . data ? . activityTypes ? . length === 1 &&
this . data ? . activityTypes ? . [ 0 ] === 'DIVIDEND'
@ -215,7 +276,7 @@ export class GfImportActivitiesDialogComponent {
}
protected onCancel() {
this . dialogRef . close ( ) ;
this . dialogRef . close ( { withRefresh : false } ) ;
}
protected async onImportActivities() {
@ -248,25 +309,26 @@ export class GfImportActivitiesDialogComponent {
}
) ;
} finally {
this . dialogRef . close ( ) ;
this . dialogRef . close ( { withRefresh : true } ) ;
}
}
protected onFilesDropp ed ( {
files ,
protected onFileSelect ed ( {
file ,
stepper
} : {
files : FileList ;
file : File ;
stepper : MatStepper ;
} ) {
if ( files . length === 0 || this . importStep !== ImportStep . UPLOAD_FILE ) {
if ( this . importStep !== ImportStep . UPLOAD_FILE ) {
return ;
}
this . handleFile ( { stepper , file : files [ 0 ] } ) ;
this . handleFile ( { file , stepper } ) ;
}
protected onGhostfolioAiChange ( { checked } : MatCheckboxChange ) {
this . hasChangedUserSettings = true ;
this . isGhostfolioAiEnabled = checked ;
this . dataService
@ -328,33 +390,6 @@ export class GfImportActivitiesDialogComponent {
aStepper . reset ( ) ;
}
protected onSelectFile ( stepper : MatStepper ) {
const input = document . createElement ( 'input' ) ;
input . accept = this . isGhostfolioAiEnabled
? [
'application/JSON' ,
. . . GfImportActivitiesDialogComponent . FILE_EXTENSIONS_OF_GHOSTFOLIO_AI . map (
( fileExtension ) = > {
return ` . ${ fileExtension } ` ;
}
)
] . join ( ', ' )
: 'application/JSON, .csv' ;
input . type = 'file' ;
input . onchange = ( event ) = > {
// Getting the file reference
const file = ( event . target as HTMLInputElement ) . files ? . [ 0 ] ;
if ( file ) {
this . handleFile ( { file , stepper } ) ;
}
} ;
input . click ( ) ;
}
protected updateSelection ( activities : Activity [ ] ) {
this . selectedActivities = activities . filter ( ( { error } ) = > {
return ! error ;