Browse Source

Merge 95c013a52a into e2382834c3

pull/5473/merge
EvilMax03 20 hours ago
committed by GitHub
parent
commit
85618f44a1
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 154
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
  2. 3
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html

154
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts

@ -1,13 +1,14 @@
import { CreateTagDto } from '@ghostfolio/api/app/endpoints/tags/create-tag.dto';
import { CreateAccountWithBalancesDto } from '@ghostfolio/api/app/import/create-account-with-balances.dto';
import { CreateAssetProfileWithMarketDataDto } from '@ghostfolio/api/app/import/create-asset-profile-with-market-data.dto';
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { GfDialogFooterComponent } from '@ghostfolio/client/components/dialog-footer/dialog-footer.component';
import { GfDialogHeaderComponent } from '@ghostfolio/client/components/dialog-header/dialog-header.component';
import { GfFileDropModule } from '@ghostfolio/client/directives/file-drop/file-drop.module';
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
import { DataService } from '@ghostfolio/client/services/data.service';
import { ImportActivitiesService } from '@ghostfolio/client/services/import-activities.service';
import { GfDialogFooterComponent } from '../../../../components/dialog-footer/dialog-footer.component';
import { GfDialogHeaderComponent } from '../../../../components/dialog-header/dialog-header.component';
import { GfFileDropModule } from '../../../../directives/file-drop/file-drop.module';
import { GfSymbolModule } from '../../../../pipes/symbol/symbol.module';
import { DataService } from '../../../../services/data.service';
import { ImportActivitiesService } from '../../../../services/import-activities.service';
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table';
@ -209,7 +210,13 @@ export class GfImportActivitiesDialog implements OnDestroy {
return;
}
if (files.length === 1) {
// Single file import (original behavior)
this.handleFile({ stepper, file: files[0] });
} else {
// Multiple files import (bulk import)
this.handleMultipleFiles({ stepper, files });
}
}
public onImportStepChange(event: StepperSelectionEvent) {
@ -257,11 +264,19 @@ export class GfImportActivitiesDialog implements OnDestroy {
const input = document.createElement('input');
input.accept = 'application/JSON, .csv';
input.type = 'file';
input.multiple = true; // Allow multiple file selection
input.onchange = (event) => {
// Getting the file reference
const file = (event.target as HTMLInputElement).files[0];
this.handleFile({ file, stepper });
const files = (event.target as HTMLInputElement).files;
if (files && files.length > 0) {
if (files.length === 1) {
// Single file import (original behavior)
this.handleFile({ file: files[0], stepper });
} else {
// Multiple files import (bulk import)
this.handleMultipleFiles({ stepper, files });
}
}
};
input.click();
@ -278,6 +293,127 @@ export class GfImportActivitiesDialog implements OnDestroy {
this.unsubscribeSubject.complete();
}
private async handleMultipleFiles({
files,
stepper
}: {
files: FileList;
stepper: MatStepper;
}): Promise<void> {
this.snackBar.open(`⏳ Processing ${files.length} files...`);
const allActivities: CreateOrderDto[] = [];
const allAccounts: CreateAccountWithBalancesDto[] = [];
const allAssetProfiles: CreateAssetProfileWithMarketDataDto[] = [];
const allTags: CreateTagDto[] = [];
let filesProcessed = 0;
let hasErrors = false;
for (let i = 0; i < files.length; i++) {
const file = files[i];
const fileExtension = file.name.split('.').pop()?.toLowerCase();
if (fileExtension !== 'json') {
continue;
}
try {
const fileContent = await this.readFileContent(file);
const content = JSON.parse(fileContent);
// Merge accounts, avoiding duplicates
if (content.accounts) {
content.accounts.forEach((account: CreateAccountWithBalancesDto) => {
if (!allAccounts.find(a => a.id === account.id)) {
allAccounts.push(account);
}
});
}
// Merge asset profiles, avoiding duplicates
if (content.assetProfiles) {
content.assetProfiles.forEach((profile: CreateAssetProfileWithMarketDataDto) => {
if (!allAssetProfiles.find(p => p.symbol === profile.symbol)) {
allAssetProfiles.push(profile);
}
});
}
// Merge tags, avoiding duplicates
if (content.tags) {
content.tags.forEach((tag: CreateTagDto) => {
if (!allTags.find(t => t.id === tag.id)) {
allTags.push(tag);
}
});
}
// Add activities
if (isArray(content.activities)) {
const cleanedActivities = content.activities.map((activity: any) => {
if (activity.id) {
delete activity.id;
}
return activity as CreateOrderDto;
});
allActivities.push(...cleanedActivities);
}
filesProcessed++;
this.snackBar.open(`⏳ Processed ${filesProcessed}/${files.length} files...`);
} catch (error) {
hasErrors = true;
}
}
if (hasErrors) {
this.snackBar.open(`⚠️ Some files could not be processed`);
}
// Store merged data
this.accounts = allAccounts;
this.assetProfiles = allAssetProfiles;
this.tags = allTags;
try {
// Validate all activities at once
const { activities } = await this.importActivitiesService.importJson({
accounts: allAccounts,
activities: allActivities,
assetProfiles: allAssetProfiles,
isDryRun: true,
tags: allTags
});
this.activities = activities;
this.dataSource = new MatTableDataSource(activities.reverse());
this.totalItems = activities.length;
this.snackBar.open(`✅ Successfully processed ${filesProcessed} files with ${activities.length} activities`);
} catch (error) {
this.handleImportError({ error, activities: allActivities as any });
this.snackBar.open(`❌ Validation failed for bulk import`);
} finally {
this.importStep = ImportStep.SELECT_ACTIVITIES;
this.snackBar.dismiss();
this.updateSelection(this.activities);
stepper.next();
this.changeDetectorRef.markForCheck();
}
}
private readFileContent(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (event) => resolve(event.target?.result as string);
reader.onerror = () => reject(reader.error);
reader.readAsText(file, 'UTF-8');
});
}
private async handleFile({
file,
stepper

3
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html

@ -80,7 +80,8 @@
class="align-items-center d-flex flex-column justify-content-center"
>
<ion-icon class="cloud-icon" name="cloud-upload-outline" />
<span i18n>Choose or drop a file here</span>
<span i18n>Choose or drop file(s) here</span>
<small class="mt-2 text-muted" i18n>Multiple JSON files supported for bulk import</small>
</div>
</button>
<p class="mb-0 mt-3 text-center">

Loading…
Cancel
Save