diff --git a/CHANGELOG.md b/CHANGELOG.md index f4c28f319..0f99763ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Improved the language localization for German (`de`) +- Extended and made the columns of the asset profiles sortable in the admin control +- Moved the asset profile details in the admin control panel to a dialog ## 1.181.2 - 21.08.2022 diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index 2d4edc993..cbbcda7e6 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -84,7 +84,9 @@ export class AdminService { return { dataSource, marketDataItemCount, - symbol + symbol, + countriesCount: 0, + sectorsCount: 0 }; }); @@ -95,6 +97,10 @@ export class AdminService { _count: { select: { Order: true } }, + assetClass: true, + assetSubClass: true, + countries: true, + sectors: true, dataSource: true, Order: { orderBy: [{ date: 'asc' }], @@ -106,6 +112,9 @@ export class AdminService { } }) ).map((symbolProfile) => { + const countriesCount = symbolProfile.countries + ? Object.keys(symbolProfile.countries).length + : 0; const marketDataItemCount = marketData.find((marketDataItem) => { return ( @@ -113,10 +122,17 @@ export class AdminService { marketDataItem.symbol === symbolProfile.symbol ); })?._count ?? 0; + const sectorsCount = symbolProfile.sectors + ? Object.keys(symbolProfile.sectors).length + : 0; return { + countriesCount, marketDataItemCount, + sectorsCount, activityCount: symbolProfile._count.Order, + assetClass: symbolProfile.assetClass, + assetSubClass: symbolProfile.assetSubClass, dataSource: symbolProfile.dataSource, date: symbolProfile.Order?.[0]?.date, symbol: symbolProfile.symbol 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 538ebb62b..fdc8b544e 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 @@ -3,17 +3,26 @@ import { ChangeDetectorRef, Component, OnDestroy, - OnInit + OnInit, + ViewChild } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { MatSort } from '@angular/material/sort'; +import { MatTableDataSource } from '@angular/material/table'; +import { ActivatedRoute, Router } from '@angular/router'; 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 { getDateFormatString } from '@ghostfolio/common/helper'; +import { DATE_FORMAT, getDateFormatString } from '@ghostfolio/common/helper'; import { UniqueAsset, User } from '@ghostfolio/common/interfaces'; import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface'; -import { DataSource, MarketData } from '@prisma/client'; +import { DataSource } from '@prisma/client'; +import { format, parseISO } from 'date-fns'; +import { DeviceDetectorService } from 'ngx-device-detector'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; +import { AssetProfileDialog } from './asset-profile-dialog/asset-profile-dialog.component'; +import { AssetProfileDialogParams } from './asset-profile-dialog/interfaces/interfaces'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, @@ -22,11 +31,26 @@ import { takeUntil } from 'rxjs/operators'; templateUrl: './admin-market-data.html' }) export class AdminMarketDataComponent implements OnDestroy, OnInit { + @ViewChild(MatSort) sort: MatSort; + public currentDataSource: DataSource; public currentSymbol: string; + public dataSource: MatTableDataSource = new MatTableDataSource(); public defaultDateFormat: string; + public deviceType: string; + public displayedColumns = [ + 'symbol', + 'dataSource', + 'assetClass', + 'assetSubClass', + 'date', + 'activityCount', + 'marketDataItemCount', + 'countriesCount', + 'sectorsCount', + 'actions' + ]; public marketData: AdminMarketDataItem[] = []; - public marketDataDetails: MarketData[] = []; public user: User; private unsubscribeSubject = new Subject(); @@ -35,8 +59,29 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit { private adminService: AdminService, private changeDetectorRef: ChangeDetectorRef, private dataService: DataService, + private deviceService: DeviceDetectorService, + private dialog: MatDialog, + private route: ActivatedRoute, + private router: Router, private userService: UserService ) { + this.route.queryParams + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((params) => { + if ( + params['assetProfileDialog'] && + params['dataSource'] && + params['dateOfFirstActivity'] && + params['symbol'] + ) { + this.openAssetProfileDialog({ + dataSource: params['dataSource'], + dateOfFirstActivity: params['dateOfFirstActivity'], + symbol: params['symbol'] + }); + } + }); + this.userService.stateChanged .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((state) => { @@ -51,6 +96,8 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit { } public ngOnInit() { + this.deviceType = this.deviceService.getDeviceInfo().deviceType; + this.fetchAdminMarketData(); } @@ -75,28 +122,19 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit { .subscribe(() => {}); } - public onMarketDataChanged(withRefresh: boolean = false) { - if (withRefresh) { - this.fetchAdminMarketData(); - this.fetchAdminMarketDataBySymbol({ - dataSource: this.currentDataSource, - symbol: this.currentSymbol - }); - } - } - - public setCurrentProfile({ dataSource, symbol }: UniqueAsset) { - this.marketDataDetails = []; - - if (this.currentSymbol === symbol) { - this.currentDataSource = undefined; - this.currentSymbol = ''; - } else { - this.currentDataSource = dataSource; - this.currentSymbol = symbol; - - this.fetchAdminMarketDataBySymbol({ dataSource, symbol }); - } + public onOpenAssetProfileDialog({ + dataSource, + dateOfFirstActivity, + symbol + }: UniqueAsset & { dateOfFirstActivity: string }) { + this.router.navigate([], { + queryParams: { + dataSource, + symbol, + assetProfileDialog: true, + dateOfFirstActivity: format(parseISO(dateOfFirstActivity), DATE_FORMAT) + } + }); } public ngOnDestroy() { @@ -109,20 +147,48 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit { .fetchAdminMarketData() .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(({ marketData }) => { - this.marketData = marketData; + this.dataSource = new MatTableDataSource(marketData); + + this.dataSource.sort = this.sort; this.changeDetectorRef.markForCheck(); }); } - private fetchAdminMarketDataBySymbol({ dataSource, symbol }: UniqueAsset) { - this.adminService - .fetchAdminMarketDataBySymbol({ dataSource, symbol }) + private openAssetProfileDialog({ + dataSource, + dateOfFirstActivity, + symbol + }: { + dataSource: DataSource; + dateOfFirstActivity: string; + symbol: string; + }) { + this.userService + .get() .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(({ marketData }) => { - this.marketDataDetails = marketData; - - this.changeDetectorRef.markForCheck(); + .subscribe((user) => { + this.user = user; + + const dialogRef = this.dialog.open(AssetProfileDialog, { + autoFocus: false, + data: { + dataSource, + dateOfFirstActivity, + symbol, + deviceType: this.deviceType, + locale: this.user?.settings?.locale + }, + height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', + width: this.deviceType === 'mobile' ? '100vw' : '50rem' + }); + + dialogRef + .afterClosed() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + this.router.navigate(['.'], { relativeTo: this.route }); + }); }); } } diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.html b/apps/client/src/app/components/admin-market-data/admin-market-data.html index 654a692a6..d116a0e23 100644 --- a/apps/client/src/app/components/admin-market-data/admin-market-data.html +++ b/apps/client/src/app/components/admin-market-data/admin-market-data.html @@ -1,76 +1,137 @@
- - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + +
SymbolData SourceFirst ActivityActivity CountHistorical Data
+ Symbol + + {{ element.symbol }} + + Data Source + + {{ element.dataSource }} + + Asset Class + + {{ element.assetClass }} + + Asset Sub Class + + {{ element.assetSubClass }} + + First Activity + + {{ (element.date | date: defaultDateFormat) ?? '' }} + + Activity Count + + {{ element.activityCount }} + + Historical Data + + {{ element.marketDataItemCount }} + + Countries Count + + {{ element.countriesCount }} + + Sectors Count + + {{ element.sectorsCount }} + + {{ item.symbol }}{{ item.dataSource }} - {{ (item.date | date: defaultDateFormat) ?? '' }} - {{ item.activityCount }}{{ item.marketDataItemCount }} - - - - - - -
- -
diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.module.ts b/apps/client/src/app/components/admin-market-data/admin-market-data.module.ts index 1a3432fff..64e7225d7 100644 --- a/apps/client/src/app/components/admin-market-data/admin-market-data.module.ts +++ b/apps/client/src/app/components/admin-market-data/admin-market-data.module.ts @@ -2,17 +2,21 @@ import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatMenuModule } from '@angular/material/menu'; -import { GfAdminMarketDataDetailModule } from '@ghostfolio/client/components/admin-market-data-detail/admin-market-data-detail.module'; +import { MatSortModule } from '@angular/material/sort'; +import { MatTableModule } from '@angular/material/table'; import { AdminMarketDataComponent } from './admin-market-data.component'; +import { GfAssetProfileDialogModule } from './asset-profile-dialog/assset-profile-dialog.module'; @NgModule({ declarations: [AdminMarketDataComponent], imports: [ CommonModule, - GfAdminMarketDataDetailModule, + GfAssetProfileDialogModule, MatButtonModule, - MatMenuModule + MatMenuModule, + MatSortModule, + MatTableModule ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) 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 new file mode 100644 index 000000000..ce1c7d599 --- /dev/null +++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.scss @@ -0,0 +1,7 @@ +:host { + display: block; + + .mat-dialog-content { + max-height: unset; + } +} 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 new file mode 100644 index 000000000..3d718cd96 --- /dev/null +++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts @@ -0,0 +1,73 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + Inject, + OnDestroy, + OnInit +} from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { AdminService } from '@ghostfolio/client/services/admin.service'; +import { UniqueAsset } from '@ghostfolio/common/interfaces'; +import { MarketData } from '@prisma/client'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +import { AssetProfileDialogParams } from './interfaces/interfaces'; + +@Component({ + host: { class: 'd-flex flex-column h-100' }, + selector: 'gf-asset-profile-dialog', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: 'asset-profile-dialog.html', + styleUrls: ['./asset-profile-dialog.component.scss'] +}) +export class AssetProfileDialog implements OnDestroy, OnInit { + public marketDataDetails: MarketData[] = []; + + private unsubscribeSubject = new Subject(); + + public constructor( + private adminService: AdminService, + private changeDetectorRef: ChangeDetectorRef, + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: AssetProfileDialogParams + ) {} + + public ngOnInit(): void { + this.initialize(); + } + + public onClose(): void { + this.dialogRef.close(); + } + + public onMarketDataChanged(withRefresh: boolean = false) { + if (withRefresh) { + this.initialize(); + } + } + + public ngOnDestroy() { + this.unsubscribeSubject.next(); + this.unsubscribeSubject.complete(); + } + + private fetchAdminMarketDataBySymbol({ dataSource, symbol }: UniqueAsset) { + this.adminService + .fetchAdminMarketDataBySymbol({ dataSource, symbol }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(({ marketData }) => { + this.marketDataDetails = marketData; + + this.changeDetectorRef.markForCheck(); + }); + } + + private initialize() { + this.fetchAdminMarketDataBySymbol({ + dataSource: this.data.dataSource, + symbol: this.data.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 new file mode 100644 index 000000000..f5f7a632d --- /dev/null +++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -0,0 +1,24 @@ + + +
+ +
+ + diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/assset-profile-dialog.module.ts b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/assset-profile-dialog.module.ts new file mode 100644 index 000000000..966883daa --- /dev/null +++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/assset-profile-dialog.module.ts @@ -0,0 +1,23 @@ +import { CommonModule } from '@angular/common'; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatDialogModule } from '@angular/material/dialog'; +import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module'; +import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module'; +import { GfAdminMarketDataDetailModule } from '../../admin-market-data-detail/admin-market-data-detail.module'; + +import { AssetProfileDialog } from './asset-profile-dialog.component'; + +@NgModule({ + declarations: [AssetProfileDialog], + imports: [ + CommonModule, + GfAdminMarketDataDetailModule, + GfDialogFooterModule, + GfDialogHeaderModule, + MatButtonModule, + MatDialogModule + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class GfAssetProfileDialogModule {} diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/interfaces/interfaces.ts new file mode 100644 index 000000000..05c70f749 --- /dev/null +++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/interfaces/interfaces.ts @@ -0,0 +1,9 @@ +import { DataSource } from '@prisma/client'; + +export interface AssetProfileDialogParams { + dateOfFirstActivity: string; + dataSource: DataSource; + deviceType: string; + locale: string; + symbol: string; +} diff --git a/apps/client/src/app/components/home-holdings/home-holdings.component.ts b/apps/client/src/app/components/home-holdings/home-holdings.component.ts index 2065da3f1..fff2529d4 100644 --- a/apps/client/src/app/components/home-holdings/home-holdings.component.ts +++ b/apps/client/src/app/components/home-holdings/home-holdings.component.ts @@ -47,7 +47,7 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { private settingsStorageService: SettingsStorageService, private userService: UserService ) { - route.queryParams + this.route.queryParams .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((params) => { if ( diff --git a/apps/client/src/app/components/home-market/home-market.component.ts b/apps/client/src/app/components/home-market/home-market.component.ts index b6395c2ac..425ef0df7 100644 --- a/apps/client/src/app/components/home-market/home-market.component.ts +++ b/apps/client/src/app/components/home-market/home-market.component.ts @@ -21,6 +21,8 @@ import { takeUntil } from 'rxjs/operators'; export class HomeMarketComponent implements OnDestroy, OnInit { public benchmarks: Benchmark[]; public fearAndGreedIndex: number; + public fearLabel = $localize`Fear`; + public greedLabel = $localize`Greed`; public hasPermissionToAccessFearAndGreedIndex: boolean; public historicalData: HistoricalDataItem[]; public info: InfoItem; diff --git a/apps/client/src/app/components/home-market/home-market.html b/apps/client/src/app/components/home-market/home-market.html index b0a01a43c..fe6cba147 100644 --- a/apps/client/src/app/components/home-market/home-market.html +++ b/apps/client/src/app/components/home-market/home-market.html @@ -9,13 +9,13 @@ class="mb-3" symbol="Fear & Greed Index" yMax="100" - yMaxLabel="Greed" yMin="0" - yMinLabel="Fear" [historicalDataItems]="historicalData" [locale]="user?.settings?.locale" [showXAxis]="true" [showYAxis]="true" + [yMaxLabel]="greedLabel" + [yMinLabel]="fearLabel" > apps/client/src/app/components/admin-market-data/admin-market-data.html - 55 + 122 apps/client/src/app/components/admin-users/admin-users.html @@ -222,7 +222,7 @@ apps/client/src/app/components/admin-market-data/admin-market-data.html - 7 + 14 apps/client/src/app/components/positions-table/positions-table.component.html @@ -242,7 +242,7 @@ apps/client/src/app/components/admin-market-data/admin-market-data.html - 8 + 23 apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html @@ -406,7 +406,7 @@ Erste Aktivität apps/client/src/app/components/admin-market-data/admin-market-data.html - 9 + 50 @@ -414,7 +414,7 @@ Anzahl Aktivitäten apps/client/src/app/components/admin-market-data/admin-market-data.html - 10 + 59 apps/client/src/app/components/admin-overview/admin-overview.html @@ -426,7 +426,7 @@ Historische Daten apps/client/src/app/components/admin-market-data/admin-market-data.html - 11 + 68 @@ -434,7 +434,7 @@ Daten einholen apps/client/src/app/components/admin-market-data/admin-market-data.html - 42 + 109 @@ -514,7 +514,7 @@ Profildaten einholen apps/client/src/app/components/admin-market-data/admin-market-data.html - 48 + 115 apps/client/src/app/components/admin-overview/admin-overview.html @@ -1988,6 +1988,10 @@ Asset Class Anlageklasse + + apps/client/src/app/components/admin-market-data/admin-market-data.html + 32 + apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html 145 @@ -2380,6 +2384,10 @@ Asset Sub Class Anlageunterklasse + + apps/client/src/app/components/admin-market-data/admin-market-data.html + 41 + apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html 154 @@ -2561,6 +2569,38 @@ 296 + + Countries Count + Anzahl Länder + + apps/client/src/app/components/admin-market-data/admin-market-data.html + 77 + + + + Sectors Count + Anzahl Sektoren + + apps/client/src/app/components/admin-market-data/admin-market-data.html + 86 + + + + Fear + Angst + + apps/client/src/app/components/home-market/home-market.component.ts + 24 + + + + Greed + Gier + + apps/client/src/app/components/home-market/home-market.component.ts + 25 + + diff --git a/apps/client/src/locales/messages.xlf b/apps/client/src/locales/messages.xlf index 6d840a92e..5a3f7440a 100644 --- a/apps/client/src/locales/messages.xlf +++ b/apps/client/src/locales/messages.xlf @@ -174,7 +174,7 @@ apps/client/src/app/components/admin-market-data/admin-market-data.html - 55 + 122 apps/client/src/app/components/admin-users/admin-users.html @@ -207,7 +207,7 @@ apps/client/src/app/components/admin-market-data/admin-market-data.html - 7 + 14 apps/client/src/app/components/positions-table/positions-table.component.html @@ -226,7 +226,7 @@ apps/client/src/app/components/admin-market-data/admin-market-data.html - 8 + 23 apps/client/src/app/pages/portfolio/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.html @@ -375,14 +375,14 @@ First Activity apps/client/src/app/components/admin-market-data/admin-market-data.html - 9 + 50 Activity Count apps/client/src/app/components/admin-market-data/admin-market-data.html - 10 + 59 apps/client/src/app/components/admin-overview/admin-overview.html @@ -393,14 +393,14 @@ Historical Data apps/client/src/app/components/admin-market-data/admin-market-data.html - 11 + 68 Gather Data apps/client/src/app/components/admin-market-data/admin-market-data.html - 42 + 109 @@ -470,7 +470,7 @@ Gather Profile Data apps/client/src/app/components/admin-market-data/admin-market-data.html - 48 + 115 apps/client/src/app/components/admin-overview/admin-overview.html @@ -1781,6 +1781,10 @@ Asset Class + + apps/client/src/app/components/admin-market-data/admin-market-data.html + 32 + apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html 145 @@ -2108,6 +2112,10 @@ Asset Sub Class + + apps/client/src/app/components/admin-market-data/admin-market-data.html + 41 + apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html 154 @@ -2287,6 +2295,34 @@ 29 + + Sectors Count + + apps/client/src/app/components/admin-market-data/admin-market-data.html + 86 + + + + Countries Count + + apps/client/src/app/components/admin-market-data/admin-market-data.html + 77 + + + + Fear + + apps/client/src/app/components/home-market/home-market.component.ts + 24 + + + + Greed + + apps/client/src/app/components/home-market/home-market.component.ts + 25 + + \ No newline at end of file diff --git a/libs/common/src/lib/interfaces/admin-market-data.interface.ts b/libs/common/src/lib/interfaces/admin-market-data.interface.ts index 826cca861..437b2975d 100644 --- a/libs/common/src/lib/interfaces/admin-market-data.interface.ts +++ b/libs/common/src/lib/interfaces/admin-market-data.interface.ts @@ -1,12 +1,16 @@ -import { DataSource } from '@prisma/client'; +import { AssetClass, AssetSubClass, DataSource } from '@prisma/client'; export interface AdminMarketData { marketData: AdminMarketDataItem[]; } export interface AdminMarketDataItem { + assetClass?: AssetClass; + assetSubClass?: AssetSubClass; + countriesCount: number; dataSource: DataSource; date?: Date; - marketDataItemCount?: number; + marketDataItemCount: number; + sectorsCount: number; symbol: string; }