diff --git a/CHANGELOG.md b/CHANGELOG.md index aed13fe01..8e7a08306 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added pagination to the users table of the admin control panel +### Changed + +- Extracted the historical market data editor to a reusable component + ## 2.125.0 - 2024-11-30 ### Changed 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/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..7057aad83 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 @@ -3,5 +3,9 @@ .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..4fdc22986 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,15 +1,17 @@ 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, + LineChartItem, + User } from '@ghostfolio/common/interfaces'; import { translate } from '@ghostfolio/ui/i18n'; @@ -23,7 +25,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,7 +32,6 @@ 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'; @@ -75,11 +75,13 @@ export class AssetProfileDialog implements OnDestroy, OnInit { }; public currencies: string[] = []; public ghostfolioScraperApiSymbolPrefix = ghostfolioScraperApiSymbolPrefix; + public historicalDataItems: LineChartItem[]; public isBenchmark = false; - public marketDataDetails: MarketData[] = []; + public marketDataItems: 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 +98,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit { public dialogRef: MatDialogRef<AssetProfileDialog>, private formBuilder: FormBuilder, private notificationService: NotificationService, - private snackBar: MatSnackBar + private userService: UserService ) {} public ngOnInit() { @@ -109,6 +111,16 @@ export class AssetProfileDialog implements OnDestroy, OnInit { } public initialize() { + this.historicalDataItems = undefined; + + this.userService.stateChanged + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((state) => { + if (state?.user) { + this.user = state.user; + } + }); + this.adminService .fetchAdminMarketDataBySymbol({ dataSource: this.data.dataSource, @@ -121,10 +133,19 @@ export class AssetProfileDialog implements OnDestroy, OnInit { this.assetProfileClass = translate(this.assetProfile?.assetClass); this.assetProfileSubClass = translate(this.assetProfile?.assetSubClass); this.countries = {}; + this.isBenchmark = this.benchmarks.some(({ id }) => { return id === this.assetProfile.id; }); - this.marketDataDetails = marketData; + + this.historicalDataItems = marketData.map(({ date, marketPrice }) => { + return { + date: format(date, DATE_FORMAT), + value: marketPrice + }; + }); + + this.marketDataItems = marketData; this.sectors = {}; if (this.assetProfile?.countries?.length > 0) { @@ -200,47 +221,6 @@ 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(); 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..eeb43e932 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,50 +68,28 @@ </div> <div class="flex-grow-1" mat-dialog-content> - <gf-admin-market-data-detail + <gf-line-chart + class="mb-4" + [colorScheme]="user?.settings?.colorScheme" + [historicalDataItems]="historicalDataItems" + [isAnimated]="true" + [locale]="data.locale" + [showXAxis]="true" + [showYAxis]="true" + [symbol]="data.symbol" + /> + <gf-historical-market-data-editor class="mb-3" [currency]="assetProfile?.currency" [dataSource]="data.dataSource" [dateOfFirstActivity]="assetProfile?.dateOfFirstActivity" [locale]="data.locale" - [marketData]="marketDataDetails" + [marketData]="marketDataItems" [symbol]="data.symbol" + [user]="user" (marketDataChanged)="onMarketDataChanged($event)" /> - <div class="mt-3" formGroupName="historicalData"> - <mat-form-field appearance="outline" class="w-100 without-hint"> - <mat-label> - <ng-container i18n>Historical Data</ng-container> (CSV) - </mat-label> - <textarea - cdkAutosizeMaxRows="5" - cdkTextareaAutosize - formControlName="csvString" - matInput - type="text" - (keyup.enter)="$event.stopPropagation()" - ></textarea> - </mat-form-field> - </div> - - <div class="d-flex justify-content-end mt-2"> - <button - color="accent" - mat-flat-button - type="button" - [disabled]=" - !assetProfileForm.controls['historicalData']?.controls['csvString'] - .touched || - assetProfileForm.controls['historicalData']?.controls['csvString'] - ?.value === '' - " - (click)="onImportHistoricalData()" - > - <ng-container i18n>Import</ng-container> - </button> - </div> - <div class="row"> <div class="col-6 mb-3"> <gf-value i18n size="medium" [value]="assetProfile?.symbol" diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.module.ts b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.module.ts index a872f567f..d5e14ecb5 100644 --- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.module.ts +++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.module.ts @@ -1,7 +1,8 @@ -import { GfAdminMarketDataDetailModule } from '@ghostfolio/client/components/admin-market-data-detail/admin-market-data-detail.module'; import { AdminMarketDataService } from '@ghostfolio/client/components/admin-market-data/admin-market-data.service'; import { GfAssetProfileIconComponent } from '@ghostfolio/client/components/asset-profile-icon/asset-profile-icon.component'; import { GfCurrencySelectorComponent } from '@ghostfolio/ui/currency-selector'; +import { GfHistoricalMarketDataEditorComponent } from '@ghostfolio/ui/historical-market-data-editor'; +import { GfLineChartComponent } from '@ghostfolio/ui/line-chart'; import { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart'; import { GfValueComponent } from '@ghostfolio/ui/value'; @@ -24,9 +25,10 @@ import { AssetProfileDialog } from './asset-profile-dialog.component'; imports: [ CommonModule, FormsModule, - GfAdminMarketDataDetailModule, GfAssetProfileIconComponent, GfCurrencySelectorComponent, + GfHistoricalMarketDataEditorComponent, + GfLineChartComponent, GfPortfolioProportionChartComponent, GfValueComponent, MatButtonModule, 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/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.component.ts similarity index 60% 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/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.component.ts index 6a44d0dfb..434266e1e 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/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.component.ts @@ -1,34 +1,58 @@ 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'; +import { HistoricalMarketDataEditorDialogParams } from './interfaces/interfaces'; @Component({ - host: { class: 'h-100' }, - selector: 'gf-market-data-detail-dialog', changeDetection: ChangeDetectionStrategy.OnPush, - styleUrls: ['./market-data-detail-dialog.scss'], - templateUrl: 'market-data-detail-dialog.html' + host: { class: 'h-100' }, + imports: [ + CommonModule, + FormsModule, + MatButtonModule, + MatDatepickerModule, + MatDialogModule, + MatFormFieldModule, + MatInputModule, + ReactiveFormsModule + ], + selector: 'gf-historical-market-data-editor-dialog', + schemas: [CUSTOM_ELEMENTS_SCHEMA], + standalone: true, + styleUrls: ['./historical-market-data-editor-dialog.scss'], + templateUrl: 'historical-market-data-editor-dialog.html' }) -export class MarketDataDetailDialog implements OnDestroy { +export class GfHistoricalMarketDataEditorDialogComponent implements OnDestroy { private unsubscribeSubject = new Subject<void>(); public constructor( private adminService: AdminService, private changeDetectorRef: ChangeDetectorRef, - @Inject(MAT_DIALOG_DATA) public data: MarketDataDetailDialogParams, + @Inject(MAT_DIALOG_DATA) + public data: HistoricalMarketDataEditorDialogParams, private dateAdapter: DateAdapter<any>, - public dialogRef: MatDialogRef<MarketDataDetailDialog>, + public dialogRef: MatDialogRef<GfHistoricalMarketDataEditorDialogComponent>, @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/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-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/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-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/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-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/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.scss diff --git a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/interfaces/interfaces.ts b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/interfaces/interfaces.ts similarity index 79% rename from apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/interfaces/interfaces.ts rename to libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/interfaces/interfaces.ts index 81188cd1f..4248b3fdb 100644 --- a/apps/client/src/app/components/admin-market-data-detail/market-data-detail-dialog/interfaces/interfaces.ts +++ b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/interfaces/interfaces.ts @@ -2,7 +2,7 @@ import { User } from '@ghostfolio/common/interfaces'; import { DataSource } from '@prisma/client'; -export interface MarketDataDetailDialogParams { +export interface HistoricalMarketDataEditorDialogParams { currency: string; dataSource: DataSource; dateString: string; diff --git a/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.html b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html similarity index 51% rename from apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.html rename to libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html index 617dd6962..b35e1d812 100644 --- a/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.html +++ b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.html @@ -1,14 +1,4 @@ <div> - <gf-line-chart - class="mb-4" - [colorScheme]="user?.settings?.colorScheme" - [historicalDataItems]="historicalDataItems" - [isAnimated]="true" - [locale]="locale" - [showXAxis]="true" - [showYAxis]="true" - [symbol]="symbol" - /> @for (itemByMonth of marketDataByMonth | keyvalue; track itemByMonth) { <div class="d-flex"> <div class="date px-1 text-nowrap">{{ itemByMonth.key }}</div> @@ -43,4 +33,42 @@ </div> </div> } + <form + class="d-flex flex-column h-100" + [formGroup]="historicalDataForm" + (ngSubmit)="onImportHistoricalData()" + > + <div class="mt-3" formGroupName="historicalData"> + <mat-form-field appearance="outline" class="w-100 without-hint"> + <mat-label> + <ng-container i18n>Historical Data</ng-container> (CSV) + </mat-label> + <textarea + cdkAutosizeMaxRows="5" + cdkTextareaAutosize + formControlName="csvString" + matInput + type="text" + (keyup.enter)="$event.stopPropagation()" + ></textarea> + </mat-form-field> + </div> + + <div class="d-flex justify-content-end mt-2"> + <button + color="accent" + mat-flat-button + type="button" + [disabled]=" + !historicalDataForm.controls['historicalData']?.controls['csvString'] + .touched || + historicalDataForm.controls['historicalData']?.controls['csvString'] + ?.value === '' + " + (click)="onImportHistoricalData()" + > + <ng-container i18n>Import</ng-container> + </button> + </div> + </form> </div> diff --git a/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.scss b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.scss similarity index 90% rename from apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.scss rename to libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.scss index a03533589..cc835a90e 100644 --- a/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.scss +++ b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.scss @@ -2,10 +2,6 @@ display: block; font-size: 0.9rem; - gf-line-chart { - aspect-ratio: 16/9; - } - .date { font-feature-settings: 'tnum'; font-variant-numeric: tabular-nums; diff --git a/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.ts b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts similarity index 55% rename from apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.ts rename to libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts index 1742d8307..0fce78621 100644 --- a/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.ts +++ b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor.component.ts @@ -1,4 +1,5 @@ -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, @@ -6,15 +7,22 @@ import { } from '@ghostfolio/common/helper'; import { LineChartItem, User } from '@ghostfolio/common/interfaces'; +import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, + OnDestroy, + OnInit, Output } from '@angular/core'; +import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; import { 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, @@ -29,55 +37,70 @@ import { parseISO } from 'date-fns'; import { first, last } from 'lodash'; +import ms from 'ms'; 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 { GfHistoricalMarketDataEditorDialogComponent } from './historical-market-data-editor-dialog/historical-market-data-editor-dialog.component'; +import { HistoricalMarketDataEditorDialogParams } from './historical-market-data-editor-dialog/interfaces/interfaces'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, - selector: 'gf-admin-market-data-detail', - styleUrls: ['./admin-market-data-detail.component.scss'], - templateUrl: './admin-market-data-detail.component.html' + imports: [CommonModule, MatButtonModule, MatInputModule, ReactiveFormsModule], + selector: 'gf-historical-market-data-editor', + standalone: true, + styleUrls: ['./historical-market-data-editor.component.scss'], + templateUrl: './historical-market-data-editor.component.html' }) -export class AdminMarketDataDetailComponent implements OnChanges { +export class GfHistoricalMarketDataEditorComponent + implements OnChanges, OnDestroy, 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<boolean>(); public days = Array(31); public defaultDateFormat: string; public deviceType: string; + public historicalDataForm = this.formBuilder.group({ + historicalData: this.formBuilder.group({ + csvString: '' + }) + }); public historicalDataItems: LineChartItem[]; public marketDataByMonth: { [yearMonth: string]: { [day: string]: Pick<MarketData, 'date' | 'marketPrice'> & { day: number }; }; } = {}; - public user: User; + + private static readonly HISTORICAL_DATA_TEMPLATE = `date;marketPrice\n${format( + new Date(), + DATE_FORMAT + )};123.45`; private unsubscribeSubject = new Subject<void>(); public constructor( + private adminService: AdminService, private deviceService: DeviceDetectorService, private dialog: MatDialog, - private userService: UserService + private formBuilder: FormBuilder, + private snackBar: MatSnackBar ) { 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,29 +200,84 @@ export class AdminMarketDataDetailComponent implements OnChanges { }) { const marketPrice = this.marketDataByMonth[yearMonth]?.[day]?.marketPrice; - const dialogRef = this.dialog.open(MarketDataDetailDialog, { - data: { - marketPrice, - currency: this.currency, - dataSource: this.dataSource, - dateString: `${yearMonth}-${day}`, - symbol: this.symbol, - user: this.user - } as MarketDataDetailDialogParams, - height: this.deviceType === 'mobile' ? '98vh' : '80vh', - width: this.deviceType === 'mobile' ? '100vw' : '50rem' - }); + const dialogRef = this.dialog.open( + GfHistoricalMarketDataEditorDialogComponent, + { + data: { + marketPrice, + currency: this.currency, + dataSource: this.dataSource, + dateString: `${yearMonth}-${day}`, + symbol: this.symbol, + user: this.user + } as HistoricalMarketDataEditorDialogParams, + height: this.deviceType === 'mobile' ? '98vh' : '80vh', + width: this.deviceType === 'mobile' ? '100vw' : '50rem' + } + ); dialogRef .afterClosed() .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(({ withRefresh } = { withRefresh: false }) => { - this.marketDataChanged.next(withRefresh); + this.marketDataChanged.emit(withRefresh); }); } + 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.dataSource, + marketData: { + marketData + }, + symbol: this.symbol + }) + .pipe( + catchError(({ error, message }) => { + this.snackBar.open(`${error}: ${message[0]}`, undefined, { + duration: ms('3 seconds') + }); + return EMPTY; + }), + takeUntil(this.unsubscribeSubject) + ) + .subscribe(() => { + this.initializeHistoricalDataForm(); + + this.marketDataChanged.emit(true); + }); + } catch { + this.snackBar.open( + $localize`Oops! Could not parse historical data.`, + undefined, + { duration: ms('3 seconds') } + ); + } + } + public ngOnDestroy() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); } + + private initializeHistoricalDataForm() { + this.historicalDataForm.setValue({ + historicalData: { + csvString: + GfHistoricalMarketDataEditorComponent.HISTORICAL_DATA_TEMPLATE + } + }); + } } diff --git a/libs/ui/src/lib/historical-market-data-editor/index.ts b/libs/ui/src/lib/historical-market-data-editor/index.ts new file mode 100644 index 000000000..6c7004ce9 --- /dev/null +++ b/libs/ui/src/lib/historical-market-data-editor/index.ts @@ -0,0 +1 @@ +export * from './historical-market-data-editor.component';