You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
355 lines
10 KiB
355 lines
10 KiB
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
|
|
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
|
import { ImportActivitiesService } from '@ghostfolio/client/services/import-activities.service';
|
|
import { Position } from '@ghostfolio/common/interfaces';
|
|
|
|
import {
|
|
StepperOrientation,
|
|
StepperSelectionEvent
|
|
} from '@angular/cdk/stepper';
|
|
import {
|
|
ChangeDetectionStrategy,
|
|
ChangeDetectorRef,
|
|
Component,
|
|
Inject,
|
|
OnDestroy
|
|
} from '@angular/core';
|
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
|
import { SortDirection } from '@angular/material/sort';
|
|
import { MatStepper } from '@angular/material/stepper';
|
|
import { MatTableDataSource } from '@angular/material/table';
|
|
import { AssetClass } from '@prisma/client';
|
|
import { isArray, sortBy } from 'lodash';
|
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
|
import { Subject, takeUntil } from 'rxjs';
|
|
|
|
import { ImportStep } from './enums/import-step';
|
|
import { ImportActivitiesDialogParams } from './interfaces/interfaces';
|
|
|
|
@Component({
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
selector: 'gf-import-activities-dialog',
|
|
styleUrls: ['./import-activities-dialog.scss'],
|
|
templateUrl: 'import-activities-dialog.html'
|
|
})
|
|
export class ImportActivitiesDialog implements OnDestroy {
|
|
public accounts: CreateAccountDto[] = [];
|
|
public activities: Activity[] = [];
|
|
public dataSource: MatTableDataSource<Activity>;
|
|
public details: any[] = [];
|
|
public deviceType: string;
|
|
public dialogTitle = $localize`Import Activities`;
|
|
public errorMessages: string[] = [];
|
|
public holdings: Position[] = [];
|
|
public importStep: ImportStep = ImportStep.UPLOAD_FILE;
|
|
public isLoading = false;
|
|
public maxSafeInteger = Number.MAX_SAFE_INTEGER;
|
|
public mode: 'DIVIDEND';
|
|
public selectedActivities: Activity[] = [];
|
|
public sortColumn = 'date';
|
|
public sortDirection: SortDirection = 'desc';
|
|
public stepperOrientation: StepperOrientation;
|
|
public totalItems: number;
|
|
public uniqueAssetForm: FormGroup;
|
|
|
|
private unsubscribeSubject = new Subject<void>();
|
|
|
|
public constructor(
|
|
private changeDetectorRef: ChangeDetectorRef,
|
|
@Inject(MAT_DIALOG_DATA) public data: ImportActivitiesDialogParams,
|
|
private dataService: DataService,
|
|
private deviceService: DeviceDetectorService,
|
|
private formBuilder: FormBuilder,
|
|
public dialogRef: MatDialogRef<ImportActivitiesDialog>,
|
|
private importActivitiesService: ImportActivitiesService,
|
|
private snackBar: MatSnackBar
|
|
) {}
|
|
|
|
public ngOnInit() {
|
|
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
|
this.stepperOrientation =
|
|
this.deviceType === 'mobile' ? 'vertical' : 'horizontal';
|
|
|
|
this.uniqueAssetForm = this.formBuilder.group({
|
|
uniqueAsset: [undefined, Validators.required]
|
|
});
|
|
|
|
if (
|
|
this.data?.activityTypes?.length === 1 &&
|
|
this.data?.activityTypes?.[0] === 'DIVIDEND'
|
|
) {
|
|
this.isLoading = true;
|
|
|
|
this.dialogTitle = $localize`Import Dividends`;
|
|
this.mode = 'DIVIDEND';
|
|
this.uniqueAssetForm.get('uniqueAsset').disable();
|
|
|
|
this.dataService
|
|
.fetchPositions({
|
|
filters: [
|
|
{
|
|
id: AssetClass.EQUITY,
|
|
type: 'ASSET_CLASS'
|
|
}
|
|
],
|
|
range: 'max'
|
|
})
|
|
.pipe(takeUntil(this.unsubscribeSubject))
|
|
.subscribe(({ positions }) => {
|
|
this.holdings = sortBy(positions, ({ name }) => {
|
|
return name.toLowerCase();
|
|
});
|
|
this.uniqueAssetForm.get('uniqueAsset').enable();
|
|
|
|
this.isLoading = false;
|
|
|
|
this.changeDetectorRef.markForCheck();
|
|
});
|
|
}
|
|
}
|
|
|
|
public onCancel() {
|
|
this.dialogRef.close();
|
|
}
|
|
|
|
public async onImportActivities() {
|
|
try {
|
|
this.snackBar.open('⏳ ' + $localize`Importing data...`);
|
|
|
|
await this.importActivitiesService.importSelectedActivities({
|
|
accounts: this.accounts,
|
|
activities: this.selectedActivities
|
|
});
|
|
|
|
this.snackBar.open(
|
|
'✅ ' + $localize`Import has been completed`,
|
|
undefined,
|
|
{
|
|
duration: 3000
|
|
}
|
|
);
|
|
} catch (error) {
|
|
this.snackBar.open(
|
|
$localize`Oops! Something went wrong.` +
|
|
' ' +
|
|
$localize`Please try again later.`,
|
|
$localize`Okay`,
|
|
{ duration: 3000 }
|
|
);
|
|
} finally {
|
|
this.dialogRef.close();
|
|
}
|
|
}
|
|
|
|
public onFilesDropped({
|
|
files,
|
|
stepper
|
|
}: {
|
|
files: FileList;
|
|
stepper: MatStepper;
|
|
}) {
|
|
if (files.length === 0) {
|
|
return;
|
|
}
|
|
|
|
this.handleFile({ stepper, file: files[0] });
|
|
}
|
|
|
|
public onImportStepChange(event: StepperSelectionEvent) {
|
|
if (event.selectedIndex === ImportStep.UPLOAD_FILE) {
|
|
this.importStep = ImportStep.UPLOAD_FILE;
|
|
} else if (event.selectedIndex === ImportStep.SELECT_ACTIVITIES) {
|
|
this.importStep = ImportStep.SELECT_ACTIVITIES;
|
|
}
|
|
}
|
|
|
|
public onLoadDividends(aStepper: MatStepper) {
|
|
this.uniqueAssetForm.get('uniqueAsset').disable();
|
|
|
|
const { dataSource, symbol } =
|
|
this.uniqueAssetForm.get('uniqueAsset').value;
|
|
|
|
this.dataService
|
|
.fetchDividendsImport({
|
|
dataSource,
|
|
symbol
|
|
})
|
|
.pipe(takeUntil(this.unsubscribeSubject))
|
|
.subscribe(({ activities }) => {
|
|
this.activities = activities;
|
|
this.dataSource = new MatTableDataSource(activities.reverse());
|
|
this.totalItems = activities.length;
|
|
|
|
aStepper.next();
|
|
|
|
this.changeDetectorRef.markForCheck();
|
|
});
|
|
}
|
|
|
|
public onReset(aStepper: MatStepper) {
|
|
this.details = [];
|
|
this.errorMessages = [];
|
|
this.importStep = ImportStep.SELECT_ACTIVITIES;
|
|
this.uniqueAssetForm.get('uniqueAsset').enable();
|
|
|
|
aStepper.reset();
|
|
}
|
|
|
|
public onSelectFile(stepper: MatStepper) {
|
|
const input = document.createElement('input');
|
|
input.accept = 'application/JSON, .csv';
|
|
input.type = 'file';
|
|
|
|
input.onchange = (event) => {
|
|
// Getting the file reference
|
|
const file = (event.target as HTMLInputElement).files[0];
|
|
this.handleFile({ file, stepper });
|
|
};
|
|
|
|
input.click();
|
|
}
|
|
|
|
public updateSelection(activities: Activity[]) {
|
|
this.selectedActivities = activities.filter(({ error }) => {
|
|
return !error;
|
|
});
|
|
}
|
|
|
|
public ngOnDestroy() {
|
|
this.unsubscribeSubject.next();
|
|
this.unsubscribeSubject.complete();
|
|
}
|
|
|
|
private async handleFile({
|
|
file,
|
|
stepper
|
|
}: {
|
|
file: File;
|
|
stepper: MatStepper;
|
|
}): Promise<void> {
|
|
this.snackBar.open('⏳ ' + $localize`Validating data...`);
|
|
|
|
// Setting up the reader
|
|
const reader = new FileReader();
|
|
reader.readAsText(file, 'UTF-8');
|
|
|
|
reader.onload = async (readerEvent) => {
|
|
const fileContent = readerEvent.target.result as string;
|
|
|
|
try {
|
|
if (file.name.endsWith('.json')) {
|
|
const content = JSON.parse(fileContent);
|
|
|
|
this.accounts = content.accounts;
|
|
|
|
if (!isArray(content.activities)) {
|
|
if (isArray(content.orders)) {
|
|
this.handleImportError({
|
|
activities: [],
|
|
error: {
|
|
error: {
|
|
message: [`orders needs to be renamed to activities`]
|
|
}
|
|
}
|
|
});
|
|
return;
|
|
} else {
|
|
throw new Error();
|
|
}
|
|
}
|
|
|
|
content.activities = content.activities.map((activity) => {
|
|
if (activity.id) {
|
|
delete activity.id;
|
|
}
|
|
|
|
return activity;
|
|
});
|
|
|
|
try {
|
|
const { activities } =
|
|
await this.importActivitiesService.importJson({
|
|
accounts: content.accounts,
|
|
activities: content.activities,
|
|
isDryRun: true
|
|
});
|
|
this.activities = activities;
|
|
this.dataSource = new MatTableDataSource(activities.reverse());
|
|
this.totalItems = activities.length;
|
|
} catch (error) {
|
|
console.error(error);
|
|
this.handleImportError({ error, activities: content.activities });
|
|
}
|
|
|
|
return;
|
|
} else if (file.name.endsWith('.csv')) {
|
|
const content = fileContent.split('\n').slice(1);
|
|
|
|
try {
|
|
const data = await this.importActivitiesService.importCsv({
|
|
fileContent,
|
|
isDryRun: true,
|
|
userAccounts: this.data.user.accounts
|
|
});
|
|
this.activities = data.activities;
|
|
this.dataSource = new MatTableDataSource(data.activities.reverse());
|
|
this.totalItems = data.activities.length;
|
|
} catch (error) {
|
|
console.error(error);
|
|
this.handleImportError({
|
|
activities: error?.activities ?? content,
|
|
error: {
|
|
error: { message: error?.error?.message ?? [error?.message] }
|
|
}
|
|
});
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
throw new Error();
|
|
} catch (error) {
|
|
console.error(error);
|
|
this.handleImportError({
|
|
activities: [],
|
|
error: { error: { message: ['Unexpected format'] } }
|
|
});
|
|
} finally {
|
|
this.importStep = ImportStep.SELECT_ACTIVITIES;
|
|
this.snackBar.dismiss();
|
|
|
|
stepper.next();
|
|
|
|
this.changeDetectorRef.markForCheck();
|
|
}
|
|
};
|
|
}
|
|
|
|
private handleImportError({
|
|
activities,
|
|
error
|
|
}: {
|
|
activities: any[];
|
|
error: any;
|
|
}) {
|
|
this.errorMessages = error?.error?.message;
|
|
|
|
for (const message of this.errorMessages) {
|
|
if (message.includes('activities.')) {
|
|
let [index] = message.split(' ');
|
|
index = index.replace('activities.', '');
|
|
[index] = index.split('.');
|
|
|
|
this.details.push(activities[index]);
|
|
} else {
|
|
this.details.push('');
|
|
}
|
|
}
|
|
|
|
this.changeDetectorRef.markForCheck();
|
|
}
|
|
}
|