Compare commits

...

2 Commits

Author SHA1 Message Date
Attila Cseh 138d867e8d
Feature/filter asset sub class options in create activity dialog (#5404) 3 days ago
Kenrick Tandrian e6aa580fae
Feature/extend watchlist endpoint by trend50d and trend200d (#5405) 3 days ago
  1. 5
      CHANGELOG.md
  2. 15
      apps/api/src/app/endpoints/watchlist/watchlist.service.ts
  3. 6
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
  4. 7
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/interfaces/interfaces.ts
  5. 13
      apps/client/src/app/components/home-watchlist/home-watchlist.component.ts
  6. 44
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
  7. 18
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html
  8. 6
      libs/common/src/lib/interfaces/asset-class-selector-option.interface.ts
  9. 2
      libs/common/src/lib/interfaces/index.ts
  10. 2
      libs/common/src/lib/interfaces/responses/watchlist-response.interface.ts

5
CHANGELOG.md

@ -7,8 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Added
- Extended the watchlist endpoint by 50-Day and 200-Day trends (experimental)
### Changed ### Changed
- Improved the create or update activity dialog’s asset sub class selector for valuables to update the options dynamically based on the selected asset class
- Randomized the minutes of the hourly data gathering cron job - Randomized the minutes of the hourly data gathering cron job
- Refactored the dialog footer component to standalone - Refactored the dialog footer component to standalone
- Refactored the dialog header component to standalone - Refactored the dialog header component to standalone

15
apps/api/src/app/endpoints/watchlist/watchlist.service.ts

@ -116,10 +116,13 @@ export class WatchlistService {
return profile.dataSource === dataSource && profile.symbol === symbol; return profile.dataSource === dataSource && profile.symbol === symbol;
}); });
const allTimeHigh = await this.marketDataService.getMax({ const [allTimeHigh, trends] = await Promise.all([
dataSource, this.marketDataService.getMax({
symbol dataSource,
}); symbol
}),
this.benchmarkService.getBenchmarkTrends({ dataSource, symbol })
]);
const performancePercent = const performancePercent =
this.benchmarkService.calculateChangeInPercentage( this.benchmarkService.calculateChangeInPercentage(
@ -138,7 +141,9 @@ export class WatchlistService {
performancePercent, performancePercent,
date: allTimeHigh?.date date: allTimeHigh?.date
} }
} },
trend50d: trends.trend50d,
trend200d: trends.trend200d
}; };
}) })
); );

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

@ -13,6 +13,7 @@ import {
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { import {
AdminMarketDataDetails, AdminMarketDataDetails,
AssetClassSelectorOption,
AssetProfileIdentifier, AssetProfileIdentifier,
LineChartItem, LineChartItem,
ScraperConfiguration, ScraperConfiguration,
@ -82,10 +83,7 @@ import ms from 'ms';
import { EMPTY, Subject } from 'rxjs'; import { EMPTY, Subject } from 'rxjs';
import { catchError, takeUntil } from 'rxjs/operators'; import { catchError, takeUntil } from 'rxjs/operators';
import { import { AssetProfileDialogParams } from './interfaces/interfaces';
AssetClassSelectorOption,
AssetProfileDialogParams
} from './interfaces/interfaces';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,

7
apps/client/src/app/components/admin-market-data/asset-profile-dialog/interfaces/interfaces.ts

@ -1,11 +1,6 @@
import { ColorScheme } from '@ghostfolio/common/types'; import { ColorScheme } from '@ghostfolio/common/types';
import { AssetClass, AssetSubClass, DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
export interface AssetClassSelectorOption {
id: AssetClass | AssetSubClass;
label: string;
}
export interface AssetProfileDialogParams { export interface AssetProfileDialogParams {
colorScheme: ColorScheme; colorScheme: ColorScheme;

13
apps/client/src/app/components/home-watchlist/home-watchlist.component.ts

@ -7,7 +7,6 @@ import {
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { BenchmarkTrend } from '@ghostfolio/common/types';
import { GfBenchmarkComponent } from '@ghostfolio/ui/benchmark'; import { GfBenchmarkComponent } from '@ghostfolio/ui/benchmark';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
@ -137,17 +136,7 @@ export class HomeWatchlistComponent implements OnDestroy, OnInit {
.fetchWatchlist() .fetchWatchlist()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ watchlist }) => { .subscribe(({ watchlist }) => {
this.watchlist = watchlist.map( this.watchlist = watchlist;
({ dataSource, marketCondition, name, performances, symbol }) => ({
dataSource,
marketCondition,
name,
performances,
symbol,
trend50d: 'UNKNOWN' as BenchmarkTrend,
trend200d: 'UNKNOWN' as BenchmarkTrend
})
);
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });

44
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts

@ -1,8 +1,12 @@
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto'; import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { ASSET_CLASS_MAPPING } from '@ghostfolio/common/config';
import { getDateFormatString } from '@ghostfolio/common/helper'; import { getDateFormatString } from '@ghostfolio/common/helper';
import { LookupItem } from '@ghostfolio/common/interfaces'; import {
AssetClassSelectorOption,
LookupItem
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo'; import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo';
import { translate } from '@ghostfolio/ui/i18n'; import { translate } from '@ghostfolio/ui/i18n';
@ -37,7 +41,7 @@ import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { IonIcon } from '@ionic/angular/standalone'; import { IonIcon } from '@ionic/angular/standalone';
import { AssetClass, AssetSubClass, Tag, Type } from '@prisma/client'; import { AssetClass, Tag, Type } from '@prisma/client';
import { isAfter, isToday } from 'date-fns'; import { isAfter, isToday } from 'date-fns';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { calendarClearOutline, refreshOutline } from 'ionicons/icons'; import { calendarClearOutline, refreshOutline } from 'ionicons/icons';
@ -73,12 +77,16 @@ import { CreateOrUpdateActivityDialogParams } from './interfaces/interfaces';
}) })
export class GfCreateOrUpdateActivityDialog implements OnDestroy { export class GfCreateOrUpdateActivityDialog implements OnDestroy {
public activityForm: FormGroup; public activityForm: FormGroup;
public assetClasses = Object.keys(AssetClass).map((assetClass) => {
return { id: assetClass, label: translate(assetClass) }; public assetClassOptions: AssetClassSelectorOption[] = Object.keys(AssetClass)
}); .map((id) => {
public assetSubClasses = Object.keys(AssetSubClass).map((assetSubClass) => { return { id, label: translate(id) } as AssetClassSelectorOption;
return { id: assetSubClass, label: translate(assetSubClass) }; })
}); .sort((a, b) => {
return a.label.localeCompare(b.label);
});
public assetSubClassOptions: AssetClassSelectorOption[] = [];
public currencies: string[] = []; public currencies: string[] = [];
public currencyOfAssetProfile: string; public currencyOfAssetProfile: string;
public currentMarketPrice = null; public currentMarketPrice = null;
@ -273,6 +281,26 @@ export class GfCreateOrUpdateActivityDialog implements OnDestroy {
} }
}); });
this.activityForm
.get('assetClass')
.valueChanges.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((assetClass) => {
const assetSubClasses = ASSET_CLASS_MAPPING.get(assetClass) ?? [];
this.assetSubClassOptions = assetSubClasses
.map((assetSubClass) => {
return {
id: assetSubClass,
label: translate(assetSubClass)
};
})
.sort((a, b) => a.label.localeCompare(b.label));
this.activityForm.get('assetSubClass').setValue(null);
this.changeDetectorRef.markForCheck();
});
this.activityForm.get('date').valueChanges.subscribe(() => { this.activityForm.get('date').valueChanges.subscribe(() => {
if (isToday(this.activityForm.get('date').value)) { if (isToday(this.activityForm.get('date').value)) {
this.activityForm.get('updateAccountBalance').enable(); this.activityForm.get('updateAccountBalance').enable();

18
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html

@ -290,9 +290,12 @@
<mat-label i18n>Asset Class</mat-label> <mat-label i18n>Asset Class</mat-label>
<mat-select formControlName="assetClass"> <mat-select formControlName="assetClass">
<mat-option [value]="null" /> <mat-option [value]="null" />
@for (assetClass of assetClasses; track assetClass) { @for (
<mat-option [value]="assetClass.id">{{ assetClassOption of assetClassOptions;
assetClass.label track assetClassOption.id
) {
<mat-option [value]="assetClassOption.id">{{
assetClassOption.label
}}</mat-option> }}</mat-option>
} }
</mat-select> </mat-select>
@ -306,9 +309,12 @@
<mat-label i18n>Asset Sub Class</mat-label> <mat-label i18n>Asset Sub Class</mat-label>
<mat-select formControlName="assetSubClass"> <mat-select formControlName="assetSubClass">
<mat-option [value]="null" /> <mat-option [value]="null" />
@for (assetSubClass of assetSubClasses; track assetSubClass) { @for (
<mat-option [value]="assetSubClass.id">{{ assetSubClassOption of assetSubClassOptions;
assetSubClass.label track assetSubClassOption.id
) {
<mat-option [value]="assetSubClassOption.id">{{
assetSubClassOption.label
}}</mat-option> }}</mat-option>
} }
</mat-select> </mat-select>

6
libs/common/src/lib/interfaces/asset-class-selector-option.interface.ts

@ -0,0 +1,6 @@
import { AssetClass, AssetSubClass } from '@prisma/client';
export interface AssetClassSelectorOption {
id: AssetClass | AssetSubClass;
label: string;
}

2
libs/common/src/lib/interfaces/index.ts

@ -8,6 +8,7 @@ import type {
AdminMarketDataItem AdminMarketDataItem
} from './admin-market-data.interface'; } from './admin-market-data.interface';
import type { AdminUsers } from './admin-users.interface'; import type { AdminUsers } from './admin-users.interface';
import type { AssetClassSelectorOption } from './asset-class-selector-option.interface';
import type { AssetProfileIdentifier } from './asset-profile-identifier.interface'; import type { AssetProfileIdentifier } from './asset-profile-identifier.interface';
import type { BenchmarkMarketDataDetails } from './benchmark-market-data-details.interface'; import type { BenchmarkMarketDataDetails } from './benchmark-market-data-details.interface';
import type { BenchmarkProperty } from './benchmark-property.interface'; import type { BenchmarkProperty } from './benchmark-property.interface';
@ -86,6 +87,7 @@ export {
AdminUsers, AdminUsers,
AiPromptResponse, AiPromptResponse,
ApiKeyResponse, ApiKeyResponse,
AssetClassSelectorOption,
AssetProfileIdentifier, AssetProfileIdentifier,
Benchmark, Benchmark,
BenchmarkMarketDataDetails, BenchmarkMarketDataDetails,

2
libs/common/src/lib/interfaces/responses/watchlist-response.interface.ts

@ -8,5 +8,7 @@ export interface WatchlistResponse {
marketCondition: Benchmark['marketCondition']; marketCondition: Benchmark['marketCondition'];
name: string; name: string;
performances: Benchmark['performances']; performances: Benchmark['performances'];
trend50d: Benchmark['trend50d'];
trend200d: Benchmark['trend200d'];
})[]; })[];
} }

Loading…
Cancel
Save