diff --git a/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.module.ts b/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.module.ts deleted file mode 100644 index 9f4e1b3bc..000000000 --- a/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { GfLineChartComponent } from '@ghostfolio/ui/line-chart'; - -import { CommonModule } from '@angular/common'; -import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; - -import { AdminMarketDataDetailComponent } from './admin-market-data-detail.component'; -import { GfMarketDataDetailDialogModule } from './market-data-detail-dialog/market-data-detail-dialog.module'; - -@NgModule({ - declarations: [AdminMarketDataDetailComponent], - exports: [AdminMarketDataDetailComponent], - imports: [CommonModule, GfLineChartComponent, GfMarketDataDetailDialogModule], - schemas: [CUSTOM_ELEMENTS_SCHEMA] -}) -export class GfAdminMarketDataDetailModule {} diff --git a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.module.ts b/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.module.ts deleted file mode 100644 index f3b55d71d..000000000 --- a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.module.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { MatButtonModule } from '@angular/material/button'; -import { MatDatepickerModule } from '@angular/material/datepicker'; -import { MatDialogModule } from '@angular/material/dialog'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatInputModule } from '@angular/material/input'; - -import { MarketDataDetailDialog } from './market-data-detail-dialog.component'; - -@NgModule({ - declarations: [MarketDataDetailDialog], - imports: [ - CommonModule, - FormsModule, - MatButtonModule, - MatDatepickerModule, - MatDialogModule, - MatFormFieldModule, - MatInputModule, - ReactiveFormsModule - ], - schemas: [CUSTOM_ELEMENTS_SCHEMA] -}) -export class GfMarketDataDetailDialogModule {} diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts index 5eb869694..6f6c6edc1 100644 --- a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts +++ b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts @@ -14,6 +14,7 @@ import { } from '@ghostfolio/common/interfaces'; import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; +import { AssetProfileDialogParams } from '@ghostfolio/ui/admin-market-data-detail/interfaces/interfaces'; import { translate } from '@ghostfolio/ui/i18n'; import { SelectionModel } from '@angular/cdk/collections'; @@ -39,7 +40,6 @@ import { distinctUntilChanged, switchMap, takeUntil } from 'rxjs/operators'; import { AdminMarketDataService } from './admin-market-data.service'; import { AssetProfileDialog } from './asset-profile-dialog/asset-profile-dialog.component'; -import { AssetProfileDialogParams } from './asset-profile-dialog/interfaces/interfaces'; import { CreateAssetProfileDialog } from './create-asset-profile-dialog/create-asset-profile-dialog.component'; import { CreateAssetProfileDialogParams } from './create-asset-profile-dialog/interfaces/interfaces'; diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.scss b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.scss index b63df0134..190018a17 100644 --- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.scss +++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.scss @@ -4,4 +4,7 @@ .mat-mdc-dialog-content { max-height: unset; } + gf-line-chart { + aspect-ratio: 16/9; + } } diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts index aacf387e7..90f45f9cc 100644 --- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts +++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts @@ -1,16 +1,19 @@ import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto'; -import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-data.dto'; import { AdminMarketDataService } from '@ghostfolio/client/components/admin-market-data/admin-market-data.service'; import { NotificationService } from '@ghostfolio/client/core/notification/notification.service'; import { AdminService } from '@ghostfolio/client/services/admin.service'; import { DataService } from '@ghostfolio/client/services/data.service'; +import { UserService } from '@ghostfolio/client/services/user/user.service'; import { validateObjectForForm } from '@ghostfolio/client/util/form.util'; import { ghostfolioScraperApiSymbolPrefix } from '@ghostfolio/common/config'; import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { AdminMarketDataDetails, - AssetProfileIdentifier + AssetProfileIdentifier, + User, + LineChartItem } from '@ghostfolio/common/interfaces'; +import { AssetProfileDialogParams } from '@ghostfolio/ui/admin-market-data-detail/interfaces/interfaces'; import { translate } from '@ghostfolio/ui/i18n'; import { @@ -23,7 +26,6 @@ import { } from '@angular/core'; import { FormBuilder, FormControl, Validators } from '@angular/forms'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { MatSnackBar } from '@angular/material/snack-bar'; import { AssetClass, AssetSubClass, @@ -31,12 +33,9 @@ import { SymbolProfile } from '@prisma/client'; import { format } from 'date-fns'; -import { parse as csvToJson } from 'papaparse'; import { EMPTY, Subject } from 'rxjs'; import { catchError, takeUntil } from 'rxjs/operators'; -import { AssetProfileDialogParams } from './interfaces/interfaces'; - @Component({ host: { class: 'd-flex flex-column h-100' }, selector: 'gf-asset-profile-dialog', @@ -75,11 +74,13 @@ export class AssetProfileDialog implements OnDestroy, OnInit { }; public currencies: string[] = []; public ghostfolioScraperApiSymbolPrefix = ghostfolioScraperApiSymbolPrefix; + public historicalDataItems: LineChartItem[]; public isBenchmark = false; public marketDataDetails: MarketData[] = []; public sectors: { [name: string]: { name: string; value: number }; }; + public user: User; private static readonly HISTORICAL_DATA_TEMPLATE = `date;marketPrice\n${format( new Date(), @@ -96,7 +97,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit { public dialogRef: MatDialogRef, private formBuilder: FormBuilder, private notificationService: NotificationService, - private snackBar: MatSnackBar + private userService: UserService ) {} public ngOnInit() { @@ -109,6 +110,13 @@ export class AssetProfileDialog implements OnDestroy, OnInit { } public initialize() { + this.userService.stateChanged + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((state) => { + if (state?.user) { + this.user = state.user; + } + }); this.adminService .fetchAdminMarketDataBySymbol({ dataSource: this.data.dataSource, @@ -125,6 +133,16 @@ export class AssetProfileDialog implements OnDestroy, OnInit { return id === this.assetProfile.id; }); this.marketDataDetails = marketData; + + this.historicalDataItems = this.marketDataDetails.map( + ({ date, marketPrice }) => { + return { + date: format(date, DATE_FORMAT), + value: marketPrice + }; + } + ); + this.sectors = {}; if (this.assetProfile?.countries?.length > 0) { @@ -200,53 +218,16 @@ export class AssetProfileDialog implements OnDestroy, OnInit { .subscribe(); } - public onImportHistoricalData() { - try { - const marketData = csvToJson( - this.assetProfileForm.controls['historicalData'].controls['csvString'] - .value, - { - dynamicTyping: true, - header: true, - skipEmptyLines: true - } - ).data as UpdateMarketDataDto[]; - - this.adminService - .postMarketData({ - dataSource: this.data.dataSource, - marketData: { - marketData - }, - symbol: this.data.symbol - }) - .pipe( - catchError(({ error, message }) => { - this.snackBar.open(`${error}: ${message[0]}`, undefined, { - duration: 3000 - }); - return EMPTY; - }), - takeUntil(this.unsubscribeSubject) - ) - .subscribe(() => { - this.initialize(); - }); - } catch { - this.snackBar.open( - $localize`Oops! Could not parse historical data.`, - undefined, - { duration: 3000 } - ); - } - } - public onMarketDataChanged(withRefresh: boolean = false) { if (withRefresh) { this.initialize(); } } + public onImportHistoricalDataChanged() { + this.initialize(); + } + public onSetBenchmark({ dataSource, symbol }: AssetProfileIdentifier) { this.dataService .postBenchmark({ dataSource, symbol }) diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html index a5d2205d2..91bb31f3b 100644 --- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html +++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -68,6 +68,17 @@
+

Line

+ -
- - - Historical Data (CSV) - - - -
- -
- -
-
- @for (itemByMonth of marketDataByMonth | keyvalue; track itemByMonth) {
{{ itemByMonth.key }}
@@ -43,4 +33,42 @@
} +
+
+ + + Historical Data (CSV) + + + +
+ +
+ +
+
diff --git a/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.scss b/libs/ui/src/lib/admin-market-data-detail/admin-market-data-detail.component.scss similarity index 100% rename from apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.scss rename to libs/ui/src/lib/admin-market-data-detail/admin-market-data-detail.component.scss diff --git a/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.ts b/libs/ui/src/lib/admin-market-data-detail/admin-market-data-detail.component.ts similarity index 63% rename from apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.ts rename to libs/ui/src/lib/admin-market-data-detail/admin-market-data-detail.component.ts index 1742d8307..1625fda34 100644 --- a/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.ts +++ b/libs/ui/src/lib/admin-market-data-detail/admin-market-data-detail.component.ts @@ -1,20 +1,29 @@ -import { UserService } from '@ghostfolio/client/services/user/user.service'; +import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-data.dto'; +import { AdminService } from '@ghostfolio/client/services/admin.service'; import { DATE_FORMAT, getDateFormatString, getLocale } from '@ghostfolio/common/helper'; import { LineChartItem, User } from '@ghostfolio/common/interfaces'; +import { AssetProfileDialogParams } from '@ghostfolio/ui/admin-market-data-detail/interfaces/interfaces'; +import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, EventEmitter, + Inject, Input, OnChanges, + OnInit, Output } from '@angular/core'; -import { MatDialog } from '@angular/material/dialog'; +import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MAT_DIALOG_DATA, MatDialog } from '@angular/material/dialog'; +import { MatInputModule } from '@angular/material/input'; +import { MatSnackBar } from '@angular/material/snack-bar'; import { DataSource, MarketData } from '@prisma/client'; import { addDays, @@ -30,26 +39,32 @@ import { } from 'date-fns'; import { first, last } from 'lodash'; import { DeviceDetectorService } from 'ngx-device-detector'; -import { Subject, takeUntil } from 'rxjs'; +import { parse as csvToJson } from 'papaparse'; +import { EMPTY, Subject, takeUntil } from 'rxjs'; +import { catchError } from 'rxjs/operators'; import { MarketDataDetailDialogParams } from './market-data-detail-dialog/interfaces/interfaces'; -import { MarketDataDetailDialog } from './market-data-detail-dialog/market-data-detail-dialog.component'; +import { MarketDataDetailDialogComponent } from './market-data-detail-dialog/market-data-detail-dialog.component'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, + imports: [CommonModule, ReactiveFormsModule, MatButtonModule, MatInputModule], selector: 'gf-admin-market-data-detail', + standalone: true, styleUrls: ['./admin-market-data-detail.component.scss'], templateUrl: './admin-market-data-detail.component.html' }) -export class AdminMarketDataDetailComponent implements OnChanges { +export class AdminMarketDataDetailComponent implements OnChanges, OnInit { @Input() currency: string; @Input() dataSource: DataSource; @Input() dateOfFirstActivity: string; @Input() locale = getLocale(); @Input() marketData: MarketData[]; @Input() symbol: string; + @Input() user: User; @Output() marketDataChanged = new EventEmitter(); + @Output() updateHistoricalData = new EventEmitter(); public days = Array(31); public defaultDateFormat: string; @@ -60,24 +75,33 @@ export class AdminMarketDataDetailComponent implements OnChanges { [day: string]: Pick & { day: number }; }; } = {}; - public user: User; + + public historicalDataForm = this.formBuilder.group({ + historicalData: this.formBuilder.group({ + csvString: '' + }) + }); + + private static readonly HISTORICAL_DATA_TEMPLATE = `date;marketPrice\n${format( + new Date(), + DATE_FORMAT + )};123.45`; private unsubscribeSubject = new Subject(); public constructor( + private adminService: AdminService, + @Inject(MAT_DIALOG_DATA) public data: AssetProfileDialogParams, private deviceService: DeviceDetectorService, private dialog: MatDialog, - private userService: UserService + private snackBar: MatSnackBar, + private formBuilder: FormBuilder ) { this.deviceType = this.deviceService.getDeviceInfo().deviceType; + } - this.userService.stateChanged - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((state) => { - if (state?.user) { - this.user = state.user; - } - }); + public ngOnInit() { + this.initializeHistoricalDataForm(); } public ngOnChanges() { @@ -177,7 +201,7 @@ export class AdminMarketDataDetailComponent implements OnChanges { }) { const marketPrice = this.marketDataByMonth[yearMonth]?.[day]?.marketPrice; - const dialogRef = this.dialog.open(MarketDataDetailDialog, { + const dialogRef = this.dialog.open(MarketDataDetailDialogComponent, { data: { marketPrice, currency: this.currency, @@ -198,6 +222,56 @@ export class AdminMarketDataDetailComponent implements OnChanges { }); } + public onImportHistoricalData() { + try { + const marketData = csvToJson( + this.historicalDataForm.controls['historicalData'].controls['csvString'] + .value, + { + dynamicTyping: true, + header: true, + skipEmptyLines: true + } + ).data as UpdateMarketDataDto[]; + + this.adminService + .postMarketData({ + dataSource: this.data.dataSource, + marketData: { + marketData + }, + symbol: this.data.symbol + }) + .pipe( + catchError(({ error, message }) => { + this.snackBar.open(`${error}: ${message[0]}`, undefined, { + duration: 3000 + }); + return EMPTY; + }), + takeUntil(this.unsubscribeSubject) + ) + .subscribe(() => { + this.updateHistoricalData.emit(); + this.initializeHistoricalDataForm(); + }); + } catch { + this.snackBar.open( + $localize`Oops! Could not parse historical data.`, + undefined, + { duration: 3000 } + ); + } + } + + public initializeHistoricalDataForm() { + this.historicalDataForm.setValue({ + historicalData: { + csvString: AdminMarketDataDetailComponent.HISTORICAL_DATA_TEMPLATE + } + }); + } + public ngOnDestroy() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); diff --git a/libs/ui/src/lib/admin-market-data-detail/index.ts b/libs/ui/src/lib/admin-market-data-detail/index.ts new file mode 100644 index 000000000..b74bee25d --- /dev/null +++ b/libs/ui/src/lib/admin-market-data-detail/index.ts @@ -0,0 +1 @@ +export * from './admin-market-data-detail.component'; diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/interfaces/interfaces.ts b/libs/ui/src/lib/admin-market-data-detail/interfaces/interfaces.ts similarity index 100% rename from apps/client/src/app/components/admin-market-data/asset-profile-dialog/interfaces/interfaces.ts rename to libs/ui/src/lib/admin-market-data-detail/interfaces/interfaces.ts diff --git a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/interfaces/interfaces.ts b/libs/ui/src/lib/admin-market-data-detail/market-data-detail-dialog/interfaces/interfaces.ts similarity index 100% rename from apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/interfaces/interfaces.ts rename to libs/ui/src/lib/admin-market-data-detail/market-data-detail-dialog/interfaces/interfaces.ts diff --git a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.component.ts b/libs/ui/src/lib/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.component.ts similarity index 71% rename from apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.component.ts rename to libs/ui/src/lib/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.component.ts index 6a44d0dfb..095a4ef76 100644 --- a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.component.ts +++ b/libs/ui/src/lib/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.component.ts @@ -1,26 +1,49 @@ import { AdminService } from '@ghostfolio/client/services/admin.service'; +import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, + CUSTOM_ELEMENTS_SCHEMA, Inject, OnDestroy } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; import { DateAdapter, MAT_DATE_LOCALE } from '@angular/material/core'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { MatDatepickerModule } from '@angular/material/datepicker'; +import { + MAT_DIALOG_DATA, + MatDialogModule, + MatDialogRef +} from '@angular/material/dialog'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; import { Subject, takeUntil } from 'rxjs'; import { MarketDataDetailDialogParams } from './interfaces/interfaces'; @Component({ host: { class: 'h-100' }, + imports: [ + CommonModule, + FormsModule, + MatButtonModule, + MatDatepickerModule, + MatDialogModule, + MatFormFieldModule, + MatInputModule, + ReactiveFormsModule + ], selector: 'gf-market-data-detail-dialog', changeDetection: ChangeDetectionStrategy.OnPush, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + standalone: true, styleUrls: ['./market-data-detail-dialog.scss'], templateUrl: 'market-data-detail-dialog.html' }) -export class MarketDataDetailDialog implements OnDestroy { +export class MarketDataDetailDialogComponent implements OnDestroy { private unsubscribeSubject = new Subject(); public constructor( @@ -28,7 +51,7 @@ export class MarketDataDetailDialog implements OnDestroy { private changeDetectorRef: ChangeDetectorRef, @Inject(MAT_DIALOG_DATA) public data: MarketDataDetailDialogParams, private dateAdapter: DateAdapter, - public dialogRef: MatDialogRef, + public dialogRef: MatDialogRef, @Inject(MAT_DATE_LOCALE) private locale: string ) {} diff --git a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html b/libs/ui/src/lib/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html similarity index 100% rename from apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html rename to libs/ui/src/lib/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.html diff --git a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.scss b/libs/ui/src/lib/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.scss similarity index 100% rename from apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.scss rename to libs/ui/src/lib/admin-market-data-detail/market-data-detail-dialog/market-data-detail-dialog.scss