Browse Source

Feature/extend asset profile details dialog (#1469)

* Extend asset profile details dialog

* Update changelog
pull/1466/head^2
Thomas Kaul 2 years ago
committed by GitHub
parent
commit
3611684f17
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      CHANGELOG.md
  2. 17
      apps/api/src/app/admin/admin.service.ts
  3. 26
      apps/api/src/services/symbol-profile.service.ts
  4. 2
      apps/client/src/app/app.module.ts
  5. 4
      apps/client/src/app/components/admin-market-data/admin-market-data.component.ts
  6. 20
      apps/client/src/app/components/admin-market-data/admin-market-data.html
  7. 35
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
  8. 90
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
  9. 4
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/assset-profile-dialog.module.ts
  10. 3
      libs/common/src/lib/interfaces/admin-market-data-details.interface.ts
  11. 1
      libs/common/src/lib/interfaces/enhanced-symbol-profile.interface.ts

6
CHANGELOG.md

@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Changed
- Extended the asset profile details dialog in the admin control panel
## 1.214.0 - 19.11.2022 ## 1.214.0 - 19.11.2022
### Added ### Added

17
apps/api/src/app/admin/admin.service.ts

@ -147,7 +147,7 @@ export class AdminService {
countriesCount, countriesCount,
marketDataItemCount, marketDataItemCount,
sectorsCount, sectorsCount,
activityCount: symbolProfile._count.Order, activitiesCount: symbolProfile._count.Order,
assetClass: symbolProfile.assetClass, assetClass: symbolProfile.assetClass,
assetSubClass: symbolProfile.assetSubClass, assetSubClass: symbolProfile.assetSubClass,
dataSource: symbolProfile.dataSource, dataSource: symbolProfile.dataSource,
@ -165,8 +165,14 @@ export class AdminService {
dataSource, dataSource,
symbol symbol
}: UniqueAsset): Promise<AdminMarketDataDetails> { }: UniqueAsset): Promise<AdminMarketDataDetails> {
return { const [[assetProfile], marketData] = await Promise.all([
marketData: await this.marketDataService.marketDataItems({ this.symbolProfileService.getSymbolProfiles([
{
dataSource,
symbol
}
]),
this.marketDataService.marketDataItems({
orderBy: { orderBy: {
date: 'asc' date: 'asc'
}, },
@ -175,6 +181,11 @@ export class AdminService {
symbol symbol
} }
}) })
]);
return {
assetProfile,
marketData
}; };
} }

26
apps/api/src/services/symbol-profile.service.ts

@ -43,7 +43,12 @@ export class SymbolProfileService {
): Promise<EnhancedSymbolProfile[]> { ): Promise<EnhancedSymbolProfile[]> {
return this.prismaService.symbolProfile return this.prismaService.symbolProfile
.findMany({ .findMany({
include: { SymbolProfileOverrides: true }, include: {
_count: {
select: { Order: true }
},
SymbolProfileOverrides: true
},
where: { where: {
AND: [ AND: [
{ {
@ -69,7 +74,12 @@ export class SymbolProfileService {
): Promise<EnhancedSymbolProfile[]> { ): Promise<EnhancedSymbolProfile[]> {
return this.prismaService.symbolProfile return this.prismaService.symbolProfile
.findMany({ .findMany({
include: { SymbolProfileOverrides: true }, include: {
_count: {
select: { Order: true }
},
SymbolProfileOverrides: true
},
where: { where: {
id: { id: {
in: symbolProfileIds.map((symbolProfileId) => { in: symbolProfileIds.map((symbolProfileId) => {
@ -89,7 +99,12 @@ export class SymbolProfileService {
): Promise<EnhancedSymbolProfile[]> { ): Promise<EnhancedSymbolProfile[]> {
return this.prismaService.symbolProfile return this.prismaService.symbolProfile
.findMany({ .findMany({
include: { SymbolProfileOverrides: true }, include: {
_count: {
select: { Order: true }
},
SymbolProfileOverrides: true
},
where: { where: {
symbol: { symbol: {
in: symbols in: symbols
@ -101,12 +116,14 @@ export class SymbolProfileService {
private getSymbols( private getSymbols(
symbolProfiles: (SymbolProfile & { symbolProfiles: (SymbolProfile & {
_count: { Order: number };
SymbolProfileOverrides: SymbolProfileOverrides; SymbolProfileOverrides: SymbolProfileOverrides;
})[] })[]
): EnhancedSymbolProfile[] { ): EnhancedSymbolProfile[] {
return symbolProfiles.map((symbolProfile) => { return symbolProfiles.map((symbolProfile) => {
const item = { const item = {
...symbolProfile, ...symbolProfile,
activitiesCount: 0,
countries: this.getCountries( countries: this.getCountries(
symbolProfile?.countries as unknown as Prisma.JsonArray symbolProfile?.countries as unknown as Prisma.JsonArray
), ),
@ -115,6 +132,9 @@ export class SymbolProfileService {
symbolMapping: this.getSymbolMapping(symbolProfile) symbolMapping: this.getSymbolMapping(symbolProfile)
}; };
item.activitiesCount = symbolProfile._count.Order;
delete item._count;
if (item.SymbolProfileOverrides) { if (item.SymbolProfileOverrides) {
item.assetClass = item.assetClass =
item.SymbolProfileOverrides.assetClass ?? item.assetClass; item.SymbolProfileOverrides.assetClass ?? item.assetClass;

2
apps/client/src/app/app.module.ts

@ -13,6 +13,7 @@ import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatTooltipModule } from '@angular/material/tooltip'; import { MatTooltipModule } from '@angular/material/tooltip';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ServiceWorkerModule } from '@angular/service-worker';
import { MaterialCssVarsModule } from 'angular-material-css-vars'; import { MaterialCssVarsModule } from 'angular-material-css-vars';
import { MarkdownModule } from 'ngx-markdown'; import { MarkdownModule } from 'ngx-markdown';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@ -27,7 +28,6 @@ import { GfHeaderModule } from './components/header/header.module';
import { authInterceptorProviders } from './core/auth.interceptor'; import { authInterceptorProviders } from './core/auth.interceptor';
import { httpResponseInterceptorProviders } from './core/http-response.interceptor'; import { httpResponseInterceptorProviders } from './core/http-response.interceptor';
import { LanguageService } from './core/language.service'; import { LanguageService } from './core/language.service';
import { ServiceWorkerModule } from '@angular/service-worker';
export function NgxStripeFactory(): string { export function NgxStripeFactory(): string {
return environment.stripePublicKey; return environment.stripePublicKey;

4
apps/client/src/app/components/admin-market-data/admin-market-data.component.ts

@ -63,10 +63,10 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
'assetClass', 'assetClass',
'assetSubClass', 'assetSubClass',
'date', 'date',
'activityCount', 'activitiesCount',
'marketDataItemCount', 'marketDataItemCount',
'countriesCount',
'sectorsCount', 'sectorsCount',
'countriesCount',
'actions' 'actions'
]; ];
public filters$ = new Subject<Filter[]>(); public filters$ = new Subject<Filter[]>();

20
apps/client/src/app/components/admin-market-data/admin-market-data.html

@ -64,12 +64,12 @@
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="activityCount"> <ng-container matColumnDef="activitiesCount">
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header> <th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
<ng-container i18n>Activity Count</ng-container> <ng-container i18n>Activities Count</ng-container>
</th> </th>
<td *matCellDef="let element" class="px-1 text-right" mat-cell> <td *matCellDef="let element" class="px-1 text-right" mat-cell>
{{ element.activityCount }} {{ element.activitiesCount }}
</td> </td>
</ng-container> </ng-container>
@ -82,21 +82,21 @@
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="countriesCount"> <ng-container matColumnDef="sectorsCount">
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header> <th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
<ng-container i18n>Countries Count</ng-container> <ng-container i18n>Sectors Count</ng-container>
</th> </th>
<td *matCellDef="let element" class="px-1 text-right" mat-cell> <td *matCellDef="let element" class="px-1 text-right" mat-cell>
{{ element.countriesCount }} {{ element.sectorsCount }}
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="sectorsCount"> <ng-container matColumnDef="countriesCount">
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header> <th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
<ng-container i18n>Sectors Count</ng-container> <ng-container i18n>Countries Count</ng-container>
</th> </th>
<td *matCellDef="let element" class="px-1 text-right" mat-cell> <td *matCellDef="let element" class="px-1 text-right" mat-cell>
{{ element.sectorsCount }} {{ element.countriesCount }}
</td> </td>
</ng-container> </ng-container>
@ -146,7 +146,7 @@
</button> </button>
<button <button
mat-menu-item mat-menu-item
[disabled]="element.activityCount !== 0" [disabled]="element.activitiesCount !== 0"
(click)="onDeleteProfileData({dataSource: element.dataSource, symbol: element.symbol})" (click)="onDeleteProfileData({dataSource: element.dataSource, symbol: element.symbol})"
> >
<ng-container i18n>Delete</ng-container> <ng-container i18n>Delete</ng-container>

35
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts

@ -8,7 +8,10 @@ import {
} from '@angular/core'; } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { AdminService } from '@ghostfolio/client/services/admin.service'; import { AdminService } from '@ghostfolio/client/services/admin.service';
import { UniqueAsset } from '@ghostfolio/common/interfaces'; import {
EnhancedSymbolProfile,
UniqueAsset
} from '@ghostfolio/common/interfaces';
import { MarketData } from '@prisma/client'; import { MarketData } from '@prisma/client';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
@ -23,7 +26,14 @@ import { AssetProfileDialogParams } from './interfaces/interfaces';
styleUrls: ['./asset-profile-dialog.component.scss'] styleUrls: ['./asset-profile-dialog.component.scss']
}) })
export class AssetProfileDialog implements OnDestroy, OnInit { export class AssetProfileDialog implements OnDestroy, OnInit {
public assetProfile: EnhancedSymbolProfile;
public countries: {
[code: string]: { name: string; value: number };
};
public marketDataDetails: MarketData[] = []; public marketDataDetails: MarketData[] = [];
public sectors: {
[name: string]: { name: string; value: number };
};
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
@ -57,8 +67,29 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
this.adminService this.adminService
.fetchAdminMarketDataBySymbol({ dataSource, symbol }) .fetchAdminMarketDataBySymbol({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ marketData }) => { .subscribe(({ assetProfile, marketData }) => {
this.assetProfile = assetProfile;
this.countries = {};
this.marketDataDetails = marketData; this.marketDataDetails = marketData;
this.sectors = {};
if (assetProfile?.countries?.length > 0) {
for (const country of assetProfile.countries) {
this.countries[country.code] = {
name: country.name,
value: country.weight
};
}
}
if (assetProfile?.sectors?.length > 0) {
for (const sector of assetProfile.sectors) {
this.sectors[sector.name] = {
name: sector.name,
value: sector.weight
};
}
}
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });

90
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html

@ -2,12 +2,13 @@
mat-dialog-title mat-dialog-title
position="center" position="center"
[deviceType]="data.deviceType" [deviceType]="data.deviceType"
[title]="data.symbol" [title]="assetProfile?.name ?? data.symbol"
(closeButtonClicked)="onClose()" (closeButtonClicked)="onClose()"
></gf-dialog-header> ></gf-dialog-header>
<div class="flex-grow-1" mat-dialog-content> <div class="flex-grow-1" mat-dialog-content>
<gf-admin-market-data-detail <gf-admin-market-data-detail
class="mb-3"
[dataSource]="data.dataSource" [dataSource]="data.dataSource"
[dateOfFirstActivity]="data.dateOfFirstActivity" [dateOfFirstActivity]="data.dateOfFirstActivity"
[locale]="data.locale" [locale]="data.locale"
@ -15,6 +16,93 @@
[symbol]="data.symbol" [symbol]="data.symbol"
(marketDataChanged)="onMarketDataChanged($event)" (marketDataChanged)="onMarketDataChanged($event)"
></gf-admin-market-data-detail> ></gf-admin-market-data-detail>
<div class="row">
<div class="col-6 mb-3">
<gf-value
i18n
size="medium"
[isDate]="data.dateOfFirstActivity ? true : false"
[locale]="data.locale"
[value]="data.dateOfFirstActivity ?? '-'"
>First Buy Date</gf-value
>
</div>
<div class="col-6 mb-3">
<gf-value
i18n
size="medium"
[locale]="data.locale"
[value]="assetProfile?.activitiesCount ?? 0"
>Transactions</gf-value
>
</div>
<div class="col-6 mb-3">
<gf-value
i18n
size="medium"
[hidden]="!assetProfile?.assetClass"
[value]="assetProfile?.assetClass"
>Asset Class</gf-value
>
</div>
<div class="col-6 mb-3">
<gf-value
i18n
size="medium"
[hidden]="!assetProfile?.assetSubClass"
[value]="assetProfile?.assetSubClass"
>Asset Sub Class</gf-value
>
</div>
<ng-container
*ngIf="assetProfile?.countries?.length > 0 || assetProfile?.sectors?.length > 0"
>
<ng-container
*ngIf="assetProfile?.countries?.length === 1 && assetProfile?.sectors?.length === 1; else charts"
>
<div *ngIf="assetProfile?.sectors?.length === 1" class="col-6 mb-3">
<gf-value
i18n
size="medium"
[locale]="data.locale"
[value]="assetProfile?.sectors[0].name"
>Sector</gf-value
>
</div>
<div *ngIf="assetProfile?.countries?.length === 1" class="col-6 mb-3">
<gf-value
i18n
size="medium"
[locale]="data.locale"
[value]="assetProfile?.countries[0].name"
>Country</gf-value
>
</div>
</ng-container>
<ng-template #charts>
<div class="col-md-6 mb-3">
<div class="h5" i18n>Sectors</div>
<gf-portfolio-proportion-chart
[colorScheme]="data.colorScheme"
[isInPercent]="true"
[keys]="['name']"
[maxItems]="10"
[positions]="sectors"
></gf-portfolio-proportion-chart>
</div>
<div class="col-md-6 mb-3">
<div class="h5" i18n>Countries</div>
<gf-portfolio-proportion-chart
[colorScheme]="data.colorScheme"
[isInPercent]="true"
[keys]="['name']"
[maxItems]="10"
[positions]="countries"
></gf-portfolio-proportion-chart>
</div>
</ng-template>
</ng-container>
</div>
</div> </div>
<gf-dialog-footer <gf-dialog-footer

4
apps/client/src/app/components/admin-market-data/asset-profile-dialog/assset-profile-dialog.module.ts

@ -5,6 +5,8 @@ import { MatDialogModule } from '@angular/material/dialog';
import { GfAdminMarketDataDetailModule } from '@ghostfolio/client/components/admin-market-data-detail/admin-market-data-detail.module'; import { GfAdminMarketDataDetailModule } from '@ghostfolio/client/components/admin-market-data-detail/admin-market-data-detail.module';
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 { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module';
import { GfValueModule } from '@ghostfolio/ui/value';
import { AssetProfileDialog } from './asset-profile-dialog.component'; import { AssetProfileDialog } from './asset-profile-dialog.component';
@ -15,6 +17,8 @@ import { AssetProfileDialog } from './asset-profile-dialog.component';
GfAdminMarketDataDetailModule, GfAdminMarketDataDetailModule,
GfDialogFooterModule, GfDialogFooterModule,
GfDialogHeaderModule, GfDialogHeaderModule,
GfPortfolioProportionChartModule,
GfValueModule,
MatButtonModule, MatButtonModule,
MatDialogModule MatDialogModule
], ],

3
libs/common/src/lib/interfaces/admin-market-data-details.interface.ts

@ -1,5 +1,8 @@
import { MarketData } from '@prisma/client'; import { MarketData } from '@prisma/client';
import { EnhancedSymbolProfile } from './enhanced-symbol-profile.interface';
export interface AdminMarketDataDetails { export interface AdminMarketDataDetails {
assetProfile: EnhancedSymbolProfile;
marketData: MarketData[]; marketData: MarketData[];
} }

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

@ -5,6 +5,7 @@ import { ScraperConfiguration } from './scraper-configuration.interface';
import { Sector } from './sector.interface'; import { Sector } from './sector.interface';
export interface EnhancedSymbolProfile { export interface EnhancedSymbolProfile {
activitiesCount: number;
assetClass: AssetClass; assetClass: AssetClass;
assetSubClass: AssetSubClass; assetSubClass: AssetSubClass;
countries: Country[]; countries: Country[];

Loading…
Cancel
Save