Browse Source

Integrate in ImportActivitiesDialog

pull/1560/head
Thomas 3 years ago
parent
commit
f8bfe72f06
  1. 2
      apps/api/src/app/symbol/symbol.controller.ts
  2. 4
      apps/api/src/app/symbol/symbol.module.ts
  3. 22
      apps/api/src/app/symbol/symbol.service.ts
  4. 1
      apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts
  5. 19
      apps/client/src/app/pages/portfolio/activities/activities-page.component.ts
  6. 2
      apps/client/src/app/pages/portfolio/activities/activities-page.html
  7. 65
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
  8. 30
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html
  9. 9
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.module.ts
  10. 2
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/interfaces/interfaces.ts
  11. 2
      libs/common/src/lib/interfaces/enhanced-symbol-profile.interface.ts
  12. 28
      libs/ui/src/lib/activities-table/activities-table.component.html
  13. 11
      libs/ui/src/lib/activities-table/activities-table.component.ts

2
apps/api/src/app/symbol/symbol.controller.ts

@ -78,6 +78,8 @@ export class SymbolController {
@Get(':dataSource/:symbol/dividends') @Get(':dataSource/:symbol/dividends')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async gatherDividends( public async gatherDividends(
@Param('dataSource') dataSource: DataSource, @Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string @Param('symbol') symbol: string

4
apps/api/src/app/symbol/symbol.module.ts

@ -2,6 +2,7 @@ import { ConfigurationModule } from '@ghostfolio/api/services/configuration.modu
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { MarketDataModule } from '@ghostfolio/api/services/market-data.module'; import { MarketDataModule } from '@ghostfolio/api/services/market-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { SymbolController } from './symbol.controller'; import { SymbolController } from './symbol.controller';
@ -14,7 +15,8 @@ import { SymbolService } from './symbol.service';
ConfigurationModule, ConfigurationModule,
DataProviderModule, DataProviderModule,
MarketDataModule, MarketDataModule,
PrismaModule PrismaModule,
SymbolProfileModule
], ],
providers: [SymbolService] providers: [SymbolService]
}) })

22
apps/api/src/app/symbol/symbol.service.ts

@ -5,12 +5,14 @@ import {
IDataProviderHistoricalResponse IDataProviderHistoricalResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { MarketDataService } from '@ghostfolio/api/services/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper'; import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
import { import {
HistoricalDataItem, HistoricalDataItem,
ImportResponse ImportResponse
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { SymbolProfile } from '@prisma/client';
import { format, subDays, subYears } from 'date-fns'; import { format, subDays, subYears } from 'date-fns';
import { LookupItem } from './interfaces/lookup-item.interface'; import { LookupItem } from './interfaces/lookup-item.interface';
@ -21,6 +23,7 @@ export class SymbolService {
public constructor( public constructor(
private readonly dataProviderService: DataProviderService, private readonly dataProviderService: DataProviderService,
private readonly marketDataService: MarketDataService, private readonly marketDataService: MarketDataService,
private readonly symbolProfileService: SymbolProfileService,
private readonly yahooFinanceService: YahooFinanceService private readonly yahooFinanceService: YahooFinanceService
) {} ) {}
@ -71,15 +74,24 @@ export class SymbolService {
dataSource, dataSource,
symbol symbol
}: IDataGatheringItem): Promise<ImportResponse> { }: IDataGatheringItem): Promise<ImportResponse> {
try {
const date = new Date(); const date = new Date();
const [[assetProfile], historicalData] = await Promise.all([
this.symbolProfileService.getSymbolProfiles([
{
dataSource,
symbol
}
]),
// TODO: Use DataProviderService // TODO: Use DataProviderService
const historicalData = await this.yahooFinanceService.getDividends( this.yahooFinanceService.getDividends(
symbol, symbol,
'day', 'day',
subYears(date, 5), subYears(date, 5),
date date
); )
]);
return { return {
activities: Object.entries(historicalData[symbol]).map( activities: Object.entries(historicalData[symbol]).map(
@ -92,9 +104,10 @@ export class SymbolService {
date: parseDate(dateString), date: parseDate(dateString),
fee: 0, fee: 0,
feeInBaseCurrency: 0, feeInBaseCurrency: 0,
id: undefined, id: assetProfile.id,
isDraft: false, isDraft: false,
quantity: 0, quantity: 0,
SymbolProfile: <SymbolProfile>(<unknown>assetProfile),
symbolProfileId: undefined, symbolProfileId: undefined,
type: 'DIVIDEND', type: 'DIVIDEND',
unitPrice: historicalDataItem.marketPrice, unitPrice: historicalDataItem.marketPrice,
@ -106,6 +119,9 @@ export class SymbolService {
} }
) )
}; };
} catch {
return { activities: [] };
}
} }
public async getForDate({ public async getForDate({

1
apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts

@ -217,6 +217,7 @@ export class YahooFinanceService implements DataProviderInterface {
return response; return response;
} catch (error) { } catch (error) {
// TODO: Log error and return empty response
throw new Error( throw new Error(
`Could not get historical market data for ${aSymbol} (${this.getName()}) from ${format( `Could not get historical market data for ${aSymbol} (${this.getName()}) from ${format(
from, from,

19
apps/client/src/app/pages/portfolio/activities/activities-page.component.ts

@ -198,12 +198,21 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
}); });
} }
public onImportDividends({ dataSource, symbol }: UniqueAsset) { public onImportDividends() {
this.dataService const dialogRef = this.dialog.open(ImportActivitiesDialog, {
.fetchDividendsImport({ dataSource, symbol }) data: <ImportActivitiesDialogParams>{
activityTypes: ['DIVIDEND'],
deviceType: this.deviceType,
user: this.user
},
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
});
dialogRef
.afterClosed()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ activities }) => { .subscribe(() => {
console.log(activities); this.fetchActivities();
}); });
} }

2
apps/client/src/app/pages/portfolio/activities/activities-page.html

@ -17,7 +17,7 @@
(export)="onExport($event)" (export)="onExport($event)"
(exportDrafts)="onExportDrafts($event)" (exportDrafts)="onExportDrafts($event)"
(import)="onImport()" (import)="onImport()"
(importDividends)="onImportDividends($event)" (importDividends)="onImportDividends()"
></gf-activities-table> ></gf-activities-table>
</div> </div>
</div> </div>

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

@ -5,12 +5,16 @@ import {
Inject, Inject,
OnDestroy OnDestroy
} from '@angular/core'; } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; 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 { ImportActivitiesService } from '@ghostfolio/client/services/import-activities.service';
import { isArray } from 'lodash'; import { Position } from '@ghostfolio/common/interfaces';
import { Subject } from 'rxjs'; import { AssetClass } from '@prisma/client';
import { isArray, sortBy } from 'lodash';
import { Subject, takeUntil } from 'rxjs';
import { ImportActivitiesDialogParams } from './interfaces/interfaces'; import { ImportActivitiesDialogParams } from './interfaces/interfaces';
@ -24,20 +28,55 @@ export class ImportActivitiesDialog implements OnDestroy {
public activities: Activity[] = []; public activities: Activity[] = [];
public details: any[] = []; public details: any[] = [];
public errorMessages: string[] = []; public errorMessages: string[] = [];
public holdings: Position[] = [];
public isFileSelected = false; public isFileSelected = false;
public mode: 'DIVIDEND';
public selectedActivities: Activity[] = []; public selectedActivities: Activity[] = [];
public uniqueAssetForm: FormGroup;
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
@Inject(MAT_DIALOG_DATA) public data: ImportActivitiesDialogParams, @Inject(MAT_DIALOG_DATA) public data: ImportActivitiesDialogParams,
private dataService: DataService,
private formBuilder: FormBuilder,
public dialogRef: MatDialogRef<ImportActivitiesDialog>, public dialogRef: MatDialogRef<ImportActivitiesDialog>,
private importActivitiesService: ImportActivitiesService, private importActivitiesService: ImportActivitiesService,
private snackBar: MatSnackBar private snackBar: MatSnackBar
) {} ) {}
public ngOnInit() {} public ngOnInit() {
this.uniqueAssetForm = this.formBuilder.group({
uniqueAsset: [undefined, Validators.required]
});
if (
this.data?.activityTypes?.length === 1 &&
this.data?.activityTypes?.[0] === 'DIVIDEND'
) {
this.mode = 'DIVIDEND';
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.changeDetectorRef.markForCheck();
});
}
}
public onCancel(): void { public onCancel(): void {
this.dialogRef.close(); this.dialogRef.close();
@ -71,6 +110,24 @@ export class ImportActivitiesDialog implements OnDestroy {
} }
} }
public onLoadDividends() {
const { dataSource, symbol } =
this.uniqueAssetForm.controls['uniqueAsset'].value;
this.dataService
.fetchDividendsImport({
dataSource,
symbol
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ activities }) => {
this.activities = activities;
this.isFileSelected = true;
this.changeDetectorRef.markForCheck();
});
}
public onReset() { public onReset() {
this.details = []; this.details = [];
this.errorMessages = []; this.errorMessages = [];
@ -95,8 +152,6 @@ export class ImportActivitiesDialog implements OnDestroy {
reader.onload = async (readerEvent) => { reader.onload = async (readerEvent) => {
const fileContent = readerEvent.target.result as string; const fileContent = readerEvent.target.result as string;
console.log(fileContent);
try { try {
if (file.name.endsWith('.json')) { if (file.name.endsWith('.json')) {
const content = JSON.parse(fileContent); const content = JSON.parse(fileContent);

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

@ -7,6 +7,31 @@
<div class="flex-grow-1" mat-dialog-content> <div class="flex-grow-1" mat-dialog-content>
<ng-container *ngIf="!isFileSelected"> <ng-container *ngIf="!isFileSelected">
<ng-container *ngIf="mode === 'DIVIDEND'; else selectFile">
<form [formGroup]="uniqueAssetForm" (ngSubmit)="onLoadDividends()">
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Holding</mat-label>
<mat-select formControlName="uniqueAsset">
<mat-option
*ngFor="let holding of holdings"
[value]="{dataSource: holding.dataSource, symbol: holding.symbol}"
>{{ holding.name }}</mat-option
>
</mat-select>
</mat-form-field>
<div class="d-flex justify-content-center flex-column">
<button
color="primary"
mat-flat-button
type="submit"
[disabled]="!uniqueAssetForm.valid"
>
<span i18n>Load Dividends</span>
</button>
</div>
</form>
</ng-container>
<ng-template #selectFile>
<div class="d-flex justify-content-center flex-column"> <div class="d-flex justify-content-center flex-column">
<button <button
class="py-3" class="py-3"
@ -18,7 +43,9 @@
<span i18n>Choose File</span> <span i18n>Choose File</span>
</button> </button>
<p class="mb-0 mt-4 text-center"> <p class="mb-0 mt-4 text-center">
<span class="mr-1" i18n>The following file formats are supported:</span> <span class="mr-1" i18n
>The following file formats are supported:</span
>
<a <a
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.csv" href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.csv"
target="_blank" target="_blank"
@ -32,6 +59,7 @@
> >
</p> </p>
</div> </div>
</ng-template>
</ng-container> </ng-container>
<ng-container *ngIf="isFileSelected"> <ng-container *ngIf="isFileSelected">
<ng-container *ngIf="errorMessages.length === 0; else errorMessage"> <ng-container *ngIf="errorMessages.length === 0; else errorMessage">

9
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.module.ts

@ -1,8 +1,11 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { MatExpansionModule } from '@angular/material/expansion'; import { MatExpansionModule } from '@angular/material/expansion';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module'; import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module'; import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module'; import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
@ -13,12 +16,16 @@ import { ImportActivitiesDialog } from './import-activities-dialog.component';
declarations: [ImportActivitiesDialog], declarations: [ImportActivitiesDialog],
imports: [ imports: [
CommonModule, CommonModule,
FormsModule,
GfActivitiesTableModule, GfActivitiesTableModule,
GfDialogFooterModule, GfDialogFooterModule,
GfDialogHeaderModule, GfDialogHeaderModule,
MatButtonModule, MatButtonModule,
MatDialogModule, MatDialogModule,
MatExpansionModule MatExpansionModule,
MatFormFieldModule,
MatSelectModule,
ReactiveFormsModule
], ],
schemas: [CUSTOM_ELEMENTS_SCHEMA] schemas: [CUSTOM_ELEMENTS_SCHEMA]
}) })

2
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/interfaces/interfaces.ts

@ -1,6 +1,8 @@
import { User } from '@ghostfolio/common/interfaces'; import { User } from '@ghostfolio/common/interfaces';
import { Type } from '@prisma/client';
export interface ImportActivitiesDialogParams { export interface ImportActivitiesDialogParams {
activityTypes: Type[];
deviceType: string; deviceType: string;
user: User; user: User;
} }

2
libs/common/src/lib/interfaces/enhanced-symbol-profile.interface.ts

@ -8,7 +8,7 @@ export interface EnhancedSymbolProfile {
activitiesCount: number; activitiesCount: number;
assetClass: AssetClass; assetClass: AssetClass;
assetSubClass: AssetSubClass; assetSubClass: AssetSubClass;
comment?: string; comment: string | null;
countries: Country[]; countries: Country[];
createdAt: Date; createdAt: Date;
currency: string | null; currency: string | null;

28
libs/ui/src/lib/activities-table/activities-table.component.html

@ -117,7 +117,7 @@
<td *matCellDef="let element" class="px-1" mat-cell> <td *matCellDef="let element" class="px-1" mat-cell>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div> <div>
<span class="text-truncate">{{ element.SymbolProfile.name }}</span> <span class="text-truncate">{{ element.SymbolProfile?.name }}</span>
<span <span
*ngIf="element.isDraft" *ngIf="element.isDraft"
class="badge badge-secondary ml-1" class="badge badge-secondary ml-1"
@ -126,9 +126,9 @@
> >
</div> </div>
</div> </div>
<div *ngIf="!isUUID(element.SymbolProfile.symbol)"> <div *ngIf="!isUUID(element.SymbolProfile?.symbol)">
<small class="text-muted">{{ <small class="text-muted">{{
element.SymbolProfile.symbol | gfSymbol element.SymbolProfile?.symbol | gfSymbol
}}</small> }}</small>
</div> </div>
</td> </td>
@ -149,7 +149,7 @@
class="d-none d-lg-table-cell px-1" class="d-none d-lg-table-cell px-1"
mat-cell mat-cell
> >
{{ element.SymbolProfile.currency }} {{ element.SymbolProfile?.currency }}
</td> </td>
<td *matFooterCellDef class="d-none d-lg-table-cell px-1" mat-footer-cell> <td *matFooterCellDef class="d-none d-lg-table-cell px-1" mat-footer-cell>
{{ baseCurrency }} {{ baseCurrency }}
@ -388,6 +388,14 @@
<ion-icon class="mr-2" name="cloud-upload-outline"></ion-icon> <ion-icon class="mr-2" name="cloud-upload-outline"></ion-icon>
<span i18n>Import Activities</span> <span i18n>Import Activities</span>
</button> </button>
<button
*ngIf="hasPermissionToImportActivities"
mat-menu-item
(click)="onImportDividends()"
>
<ion-icon class="mr-2" name="cloud-upload-outline"></ion-icon>
<span i18n>Import Dividends</span>
</button>
<button <button
*ngIf="hasPermissionToExportActivities" *ngIf="hasPermissionToExportActivities"
class="align-items-center d-flex" class="align-items-center d-flex"
@ -437,18 +445,6 @@
<ion-icon class="mr-2" name="calendar-clear-outline"></ion-icon> <ion-icon class="mr-2" name="calendar-clear-outline"></ion-icon>
<span i18n>Export Draft as ICS</span> <span i18n>Export Draft as ICS</span>
</button> </button>
<button
mat-menu-item
(click)="
onImportDividends({
dataSource: element.SymbolProfile.dataSource,
symbol: element.SymbolProfile.symbol
})
"
>
<ion-icon class="mr-2" name="cloud-upload-outline"></ion-icon>
<span i18n>Import Dividend</span>
</button>
<button mat-menu-item (click)="onDeleteActivity(element.id)"> <button mat-menu-item (click)="onDeleteActivity(element.id)">
<ion-icon class="mr-2" name="trash-outline"></ion-icon> <ion-icon class="mr-2" name="trash-outline"></ion-icon>
<span i18n>Delete</span> <span i18n>Delete</span>

11
libs/ui/src/lib/activities-table/activities-table.component.ts

@ -234,8 +234,8 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
this.import.emit(); this.import.emit();
} }
public onImportDividends({ dataSource, symbol }: UniqueAsset) { public onImportDividends() {
this.importDividends.emit({ dataSource, symbol }); this.importDividends.emit();
} }
public onOpenComment(aComment: string) { public onOpenComment(aComment: string) {
@ -277,13 +277,18 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
}; };
} }
if (activity.SymbolProfile?.currency) {
fieldValueMap[activity.SymbolProfile.currency] = { fieldValueMap[activity.SymbolProfile.currency] = {
id: activity.SymbolProfile.currency, id: activity.SymbolProfile.currency,
label: activity.SymbolProfile.currency, label: activity.SymbolProfile.currency,
type: 'TAG' type: 'TAG'
}; };
}
if (!isUUID(activity.SymbolProfile.symbol)) { if (
activity.SymbolProfile?.symbol &&
!isUUID(activity.SymbolProfile.symbol)
) {
fieldValueMap[activity.SymbolProfile.symbol] = { fieldValueMap[activity.SymbolProfile.symbol] = {
id: activity.SymbolProfile.symbol, id: activity.SymbolProfile.symbol,
label: activity.SymbolProfile.symbol, label: activity.SymbolProfile.symbol,

Loading…
Cancel
Save