diff --git a/CHANGELOG.md b/CHANGELOG.md index 835c42ba7..f3eccf8cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,13 +9,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Improved the language localization for German (`de`) - Improved the language localization for Polish (`pl`) +## 2.108.0 - 2024-09-17 + +### Added + +- Added support for bonds in the import dividends dialog +- Added a _Copy link to clipboard_ action to the access table to share the portfolio +- Added the current market price column to the historical market data table of the admin control +- Introduced filters (`dataSource` and `symbol`) in the accounts endpoint + +### Changed + +- Improved the usability of the toggle component +- Switched to the accounts endpoint in the holding detail dialog +- Added a fallback in the get quotes functionality of the _EOD Historical Data_ service + ## 2.107.1 - 2024-09-12 ### Fixed -- Fixed an issue in the activities filters that occured during destructuring +- Fixed an issue in the activities filters that occurred during destructuring ## 2.107.0 - 2024-09-10 diff --git a/apps/api/src/app/account/account.controller.ts b/apps/api/src/app/account/account.controller.ts index 594a733f7..d8c3dd002 100644 --- a/apps/api/src/app/account/account.controller.ts +++ b/apps/api/src/app/account/account.controller.ts @@ -3,6 +3,8 @@ import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.servic import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.interceptor'; +import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor'; +import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service'; import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; import { @@ -26,6 +28,7 @@ import { Param, Post, Put, + Query, UseGuards, UseInterceptors } from '@nestjs/common'; @@ -44,6 +47,7 @@ export class AccountController { public constructor( private readonly accountBalanceService: AccountBalanceService, private readonly accountService: AccountService, + private readonly apiService: ApiService, private readonly impersonationService: ImpersonationService, private readonly portfolioService: PortfolioService, @Inject(REQUEST) private readonly request: RequestWithUser @@ -84,13 +88,22 @@ export class AccountController { @Get() @UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseInterceptors(RedactValuesInResponseInterceptor) + @UseInterceptors(TransformDataSourceInRequestInterceptor) public async getAllAccounts( - @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId + @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId, + @Query('dataSource') filterByDataSource?: string, + @Query('symbol') filterBySymbol?: string ): Promise { const impersonationUserId = await this.impersonationService.validateImpersonationId(impersonationId); + const filters = this.apiService.buildFiltersFromQueryParams({ + filterByDataSource, + filterBySymbol + }); + return this.portfolioService.getAccountsWithAggregations({ + filters, userId: impersonationUserId || this.request.user.id, withExcludedAccounts: true }); diff --git a/apps/api/src/app/account/account.module.ts b/apps/api/src/app/account/account.module.ts index 1c2d20216..fb89bb2b6 100644 --- a/apps/api/src/app/account/account.module.ts +++ b/apps/api/src/app/account/account.module.ts @@ -1,6 +1,7 @@ import { AccountBalanceModule } from '@ghostfolio/api/app/account-balance/account-balance.module'; import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module'; import { RedactValuesInResponseModule } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.module'; +import { ApiModule } from '@ghostfolio/api/services/api/api.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module'; @@ -16,6 +17,7 @@ import { AccountService } from './account.service'; exports: [AccountService], imports: [ AccountBalanceModule, + ApiModule, ConfigurationModule, ExchangeRateDataModule, ImpersonationModule, diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index 143d5c1ca..6c3d7c141 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -15,7 +15,11 @@ import { PROPERTY_IS_READ_ONLY_MODE, PROPERTY_IS_USER_SIGNUP_ENABLED } from '@ghostfolio/common/config'; -import { isCurrency, getCurrencyFromSymbol } from '@ghostfolio/common/helper'; +import { + getAssetProfileIdentifier, + getCurrencyFromSymbol, + isCurrency +} from '@ghostfolio/common/helper'; import { AdminData, AdminMarketData, @@ -261,6 +265,37 @@ export class AdminService { this.prismaService.symbolProfile.count({ where }) ]); + const lastMarketPrices = await this.prismaService.marketData.findMany({ + distinct: ['dataSource', 'symbol'], + orderBy: { date: 'desc' }, + select: { + dataSource: true, + marketPrice: true, + symbol: true + }, + where: { + dataSource: { + in: assetProfiles.map(({ dataSource }) => { + return dataSource; + }) + }, + symbol: { + in: assetProfiles.map(({ symbol }) => { + return symbol; + }) + } + } + }); + + const lastMarketPriceMap = new Map(); + + for (const { dataSource, marketPrice, symbol } of lastMarketPrices) { + lastMarketPriceMap.set( + getAssetProfileIdentifier({ dataSource, symbol }), + marketPrice + ); + } + let marketData: AdminMarketDataItem[] = await Promise.all( assetProfiles.map( async ({ @@ -281,6 +316,11 @@ export class AdminService { const countriesCount = countries ? Object.keys(countries).length : 0; + + const lastMarketPrice = lastMarketPriceMap.get( + getAssetProfileIdentifier({ dataSource, symbol }) + ); + const marketDataItemCount = marketDataItems.find((marketDataItem) => { return ( @@ -288,6 +328,7 @@ export class AdminService { marketDataItem.symbol === symbol ); })?._count ?? 0; + const sectorsCount = sectors ? Object.keys(sectors).length : 0; return { @@ -298,6 +339,7 @@ export class AdminService { countriesCount, dataSource, id, + lastMarketPrice, name, symbol, marketDataItemCount, @@ -511,48 +553,86 @@ export class AdminService { } private async getMarketDataForCurrencies(): Promise { - const marketDataItems = await this.prismaService.marketData.groupBy({ - _count: true, - by: ['dataSource', 'symbol'] - }); - - const marketDataPromise: Promise[] = - this.exchangeRateDataService - .getCurrencyPairs() - .map(async ({ dataSource, symbol }) => { - let activitiesCount: EnhancedSymbolProfile['activitiesCount'] = 0; - let currency: EnhancedSymbolProfile['currency'] = '-'; - let dateOfFirstActivity: EnhancedSymbolProfile['dateOfFirstActivity']; - - if (isCurrency(getCurrencyFromSymbol(symbol))) { - currency = getCurrencyFromSymbol(symbol); - ({ activitiesCount, dateOfFirstActivity } = - await this.orderService.getStatisticsByCurrency(currency)); + const currencyPairs = this.exchangeRateDataService.getCurrencyPairs(); + + const [lastMarketPrices, marketDataItems] = await Promise.all([ + this.prismaService.marketData.findMany({ + distinct: ['dataSource', 'symbol'], + orderBy: { date: 'desc' }, + select: { + dataSource: true, + marketPrice: true, + symbol: true + }, + where: { + dataSource: { + in: currencyPairs.map(({ dataSource }) => { + return dataSource; + }) + }, + symbol: { + in: currencyPairs.map(({ symbol }) => { + return symbol; + }) } + } + }), + this.prismaService.marketData.groupBy({ + _count: true, + by: ['dataSource', 'symbol'] + }) + ]); - const marketDataItemCount = - marketDataItems.find((marketDataItem) => { - return ( - marketDataItem.dataSource === dataSource && - marketDataItem.symbol === symbol - ); - })?._count ?? 0; + const lastMarketPriceMap = new Map(); - return { - activitiesCount, - currency, - dataSource, - marketDataItemCount, - symbol, - assetClass: AssetClass.LIQUIDITY, - assetSubClass: AssetSubClass.CASH, - countriesCount: 0, - date: dateOfFirstActivity, - id: undefined, - name: symbol, - sectorsCount: 0 - }; - }); + for (const { dataSource, marketPrice, symbol } of lastMarketPrices) { + lastMarketPriceMap.set( + getAssetProfileIdentifier({ dataSource, symbol }), + marketPrice + ); + } + + const marketDataPromise: Promise[] = currencyPairs.map( + async ({ dataSource, symbol }) => { + let activitiesCount: EnhancedSymbolProfile['activitiesCount'] = 0; + let currency: EnhancedSymbolProfile['currency'] = '-'; + let dateOfFirstActivity: EnhancedSymbolProfile['dateOfFirstActivity']; + + if (isCurrency(getCurrencyFromSymbol(symbol))) { + currency = getCurrencyFromSymbol(symbol); + ({ activitiesCount, dateOfFirstActivity } = + await this.orderService.getStatisticsByCurrency(currency)); + } + + const lastMarketPrice = lastMarketPriceMap.get( + getAssetProfileIdentifier({ dataSource, symbol }) + ); + + const marketDataItemCount = + marketDataItems.find((marketDataItem) => { + return ( + marketDataItem.dataSource === dataSource && + marketDataItem.symbol === symbol + ); + })?._count ?? 0; + + return { + activitiesCount, + currency, + dataSource, + lastMarketPrice, + marketDataItemCount, + symbol, + assetClass: AssetClass.LIQUIDITY, + assetSubClass: AssetSubClass.CASH, + countriesCount: 0, + date: dateOfFirstActivity, + id: undefined, + name: symbol, + sectorsCount: 0 + }; + } + ); const marketData = await Promise.all(marketDataPromise); return { marketData, count: marketData.length }; diff --git a/apps/api/src/app/portfolio/interfaces/portfolio-holding-detail.interface.ts b/apps/api/src/app/portfolio/interfaces/portfolio-holding-detail.interface.ts index 3ce23a3bc..79e4d40dc 100644 --- a/apps/api/src/app/portfolio/interfaces/portfolio-holding-detail.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/portfolio-holding-detail.interface.ts @@ -5,10 +5,9 @@ import { HistoricalDataItem } from '@ghostfolio/common/interfaces'; -import { Account, Tag } from '@prisma/client'; +import { Tag } from '@prisma/client'; export interface PortfolioHoldingDetail { - accounts: Account[]; averagePrice: number; dataProviderInfo: DataProviderInfo; dividendInBaseCurrency: number; diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 0cd602046..39ac9cc6f 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -73,7 +73,7 @@ import { parseISO, set } from 'date-fns'; -import { isEmpty, last, uniq, uniqBy } from 'lodash'; +import { isEmpty, last, uniq } from 'lodash'; import { PortfolioCalculator } from './calculator/portfolio-calculator'; import { @@ -115,12 +115,33 @@ export class PortfolioService { }): Promise { const where: Prisma.AccountWhereInput = { userId }; - const accountFilter = filters?.find(({ type }) => { + const filterByAccount = filters?.find(({ type }) => { return type === 'ACCOUNT'; - }); + })?.id; + + const filterByDataSource = filters?.find(({ type }) => { + return type === 'DATA_SOURCE'; + })?.id; - if (accountFilter) { - where.id = accountFilter.id; + const filterBySymbol = filters?.find(({ type }) => { + return type === 'SYMBOL'; + })?.id; + + if (filterByAccount) { + where.id = filterByAccount; + } + + if (filterByDataSource && filterBySymbol) { + where.Order = { + some: { + SymbolProfile: { + AND: [ + { dataSource: filterByDataSource }, + { symbol: filterBySymbol } + ] + } + } + }; } const [accounts, details] = await Promise.all([ @@ -604,7 +625,6 @@ export class PortfolioService { if (activities.length === 0) { return { - accounts: [], averagePrice: undefined, dataProviderInfo: undefined, dividendInBaseCurrency: undefined, @@ -678,15 +698,6 @@ export class PortfolioService { ); }); - const accounts: PortfolioHoldingDetail['accounts'] = uniqBy( - activitiesOfPosition.filter(({ Account }) => { - return Account; - }), - 'Account.id' - ).map(({ Account }) => { - return Account; - }); - const dividendYieldPercent = getAnnualizedPerformancePercent({ daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)), netPerformancePercentage: timeWeightedInvestment.eq(0) @@ -767,7 +778,6 @@ export class PortfolioService { } return { - accounts, firstBuyDate, marketPrice, maxPrice, @@ -862,7 +872,6 @@ export class PortfolioService { maxPrice, minPrice, SymbolProfile, - accounts: [], averagePrice: 0, dataProviderInfo: undefined, dividendInBaseCurrency: 0, diff --git a/apps/api/src/app/portfolio/rules.service.ts b/apps/api/src/app/portfolio/rules.service.ts index 6b6526144..fd9d794b2 100644 --- a/apps/api/src/app/portfolio/rules.service.ts +++ b/apps/api/src/app/portfolio/rules.service.ts @@ -1,6 +1,9 @@ import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { Rule } from '@ghostfolio/api/models/rule'; -import { UserSettings } from '@ghostfolio/common/interfaces'; +import { + PortfolioReportRule, + UserSettings +} from '@ghostfolio/common/interfaces'; import { Injectable } from '@nestjs/common'; @@ -11,19 +14,23 @@ export class RulesService { public async evaluate( aRules: Rule[], aUserSettings: UserSettings - ) { + ): Promise { return aRules.map((rule) => { - if (rule.getSettings(aUserSettings)?.isActive) { - const { evaluation, value } = rule.evaluate( - rule.getSettings(aUserSettings) - ); + const settings = rule.getSettings(aUserSettings); + + if (settings?.isActive) { + const { evaluation, value } = rule.evaluate(settings); return { evaluation, value, isActive: true, key: rule.getKey(), - name: rule.getName() + name: rule.getName(), + settings: { + thresholdMax: settings['thresholdMax'], + thresholdMin: settings['thresholdMin'] + } }; } else { return { diff --git a/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts b/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts index cf2fd42de..8b2a1828b 100644 --- a/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts +++ b/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts @@ -18,6 +18,7 @@ import { } from '@ghostfolio/common/config'; import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper'; import { DataProviderInfo } from '@ghostfolio/common/interfaces'; +import { MarketState } from '@ghostfolio/common/types'; import { Injectable, Logger } from '@nestjs/common'; import { @@ -229,7 +230,12 @@ export class EodHistoricalDataService implements DataProviderInterface { } ).json(); - const quotes = + const quotes: { + close: number; + code: string; + previousClose: number; + timestamp: number; + }[] = eodHistoricalDataSymbols.length === 1 ? [realTimeResponse] : realTimeResponse; @@ -243,7 +249,7 @@ export class EodHistoricalDataService implements DataProviderInterface { }) ); - for (const { close, code, timestamp } of quotes) { + for (const { close, code, previousClose, timestamp } of quotes) { let currency: string; if (this.isForex(code)) { @@ -267,15 +273,21 @@ export class EodHistoricalDataService implements DataProviderInterface { } } - if (isNumber(close)) { + if (isNumber(close) || isNumber(previousClose)) { + const marketPrice: number = isNumber(close) ? close : previousClose; + let marketState: MarketState = 'closed'; + + if (this.isForex(code) || isToday(new Date(timestamp * 1000))) { + marketState = 'open'; + } else if (!isNumber(close)) { + marketState = 'delayed'; + } + response[this.convertFromEodSymbol(code)] = { currency, - dataSource: this.getName(), - marketPrice: close, - marketState: - this.isForex(code) || isToday(new Date(timestamp * 1000)) - ? 'open' - : 'closed' + marketPrice, + marketState, + dataSource: this.getName() }; } else { Logger.error( diff --git a/apps/client/src/app/components/access-table/access-table.component.html b/apps/client/src/app/components/access-table/access-table.component.html index b1befc8c9..e625cbf75 100644 --- a/apps/client/src/app/components/access-table/access-table.component.html +++ b/apps/client/src/app/components/access-table/access-table.component.html @@ -35,11 +35,9 @@ @if (element.type === 'PUBLIC') { } @@ -58,6 +56,11 @@ + @if (element.type === 'PUBLIC') { + + } diff --git a/apps/client/src/app/components/access-table/access-table.component.ts b/apps/client/src/app/components/access-table/access-table.component.ts index 7772451d4..3d47c6087 100644 --- a/apps/client/src/app/components/access-table/access-table.component.ts +++ b/apps/client/src/app/components/access-table/access-table.component.ts @@ -3,6 +3,7 @@ import { NotificationService } from '@ghostfolio/client/core/notification/notifi import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config'; import { Access } from '@ghostfolio/common/interfaces'; +import { Clipboard } from '@angular/cdk/clipboard'; import { ChangeDetectionStrategy, Component, @@ -31,7 +32,10 @@ export class AccessTableComponent implements OnChanges, OnInit { public defaultLanguageCode = DEFAULT_LANGUAGE_CODE; public displayedColumns = []; - public constructor(private notificationService: NotificationService) {} + public constructor( + private clipboard: Clipboard, + private notificationService: NotificationService + ) {} public ngOnInit() {} @@ -47,6 +51,14 @@ export class AccessTableComponent implements OnChanges, OnInit { } } + public getPublicUrl(aId: string): string { + return `${this.baseUrl}/${this.defaultLanguageCode}/p/${aId}`; + } + + public onCopyToClipboard(aId: string): void { + this.clipboard.copy(this.getPublicUrl(aId)); + } + public onDeleteAccess(aId: string) { this.notificationService.confirm({ confirmFn: () => { diff --git a/apps/client/src/app/components/access-table/access-table.module.ts b/apps/client/src/app/components/access-table/access-table.module.ts index 2ace3cfc1..4cbc7b580 100644 --- a/apps/client/src/app/components/access-table/access-table.module.ts +++ b/apps/client/src/app/components/access-table/access-table.module.ts @@ -1,3 +1,4 @@ +import { ClipboardModule } from '@angular/cdk/clipboard'; import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; @@ -11,6 +12,7 @@ import { AccessTableComponent } from './access-table.component'; declarations: [AccessTableComponent], exports: [AccessTableComponent], imports: [ + ClipboardModule, CommonModule, MatButtonModule, MatMenuModule, 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 98a1d0480..549708d87 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 @@ -142,6 +142,7 @@ export class AdminMarketDataComponent 'dataSource', 'assetClass', 'assetSubClass', + 'lastMarketPrice', 'date', 'activitiesCount', 'marketDataItemCount', 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 f3b2d8ddd..a151d1cd0 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 @@ -99,6 +99,21 @@ + + + Market Price + + +
+ +
+ +
+ First Activity 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 224e3506b..161847f99 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 @@ -1,6 +1,7 @@ import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module'; import { GfActivitiesFilterComponent } from '@ghostfolio/ui/activities-filter'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; +import { GfValueComponent } from '@ghostfolio/ui/value'; import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; @@ -27,6 +28,7 @@ import { GfCreateAssetProfileDialogModule } from './create-asset-profile-dialog/ GfCreateAssetProfileDialogModule, GfPremiumIndicatorComponent, GfSymbolModule, + GfValueComponent, MatButtonModule, MatCheckboxModule, MatMenuModule, diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts index 70cd08874..8541f65ac 100644 --- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts +++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts @@ -9,6 +9,7 @@ import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper'; import { DataProviderInfo, EnhancedSymbolProfile, + Filter, LineChartItem, User } from '@ghostfolio/common/interfaces'; @@ -152,6 +153,11 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { tags: [] }); + const filters: Filter[] = [ + { id: this.data.dataSource, type: 'DATA_SOURCE' }, + { id: this.data.symbol, type: 'SYMBOL' } + ]; + this.tagsAvailable = tags.map(({ id, name }) => { return { id, @@ -173,12 +179,20 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { .subscribe(); }); + this.dataService + .fetchAccounts({ + filters + }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(({ accounts }) => { + this.accounts = accounts; + + this.changeDetectorRef.markForCheck(); + }); + this.dataService .fetchActivities({ - filters: [ - { id: this.data.dataSource, type: 'DATA_SOURCE' }, - { id: this.data.symbol, type: 'SYMBOL' } - ], + filters, sortColumn: this.sortColumn, sortDirection: this.sortDirection }) @@ -197,7 +211,6 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { .pipe(takeUntil(this.unsubscribeSubject)) .subscribe( ({ - accounts, averagePrice, dataProviderInfo, dividendInBaseCurrency, @@ -219,7 +232,6 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { transactionCount, value }) => { - this.accounts = accounts; this.averagePrice = averagePrice; this.benchmarkDataItems = []; this.countries = {}; diff --git a/apps/client/src/app/components/rule/rule-settings-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/rule/rule-settings-dialog/interfaces/interfaces.ts new file mode 100644 index 000000000..a409ab503 --- /dev/null +++ b/apps/client/src/app/components/rule/rule-settings-dialog/interfaces/interfaces.ts @@ -0,0 +1,5 @@ +import { PortfolioReportRule } from '@ghostfolio/common/interfaces'; + +export interface IRuleSettingsDialogParams { + rule: PortfolioReportRule; +} diff --git a/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.component.ts b/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.component.ts new file mode 100644 index 000000000..41ebf49db --- /dev/null +++ b/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.component.ts @@ -0,0 +1,40 @@ +import { PortfolioReportRule } from '@ghostfolio/common/interfaces'; + +import { CommonModule } from '@angular/common'; +import { Component, Inject } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { + MAT_DIALOG_DATA, + MatDialogModule, + MatDialogRef +} from '@angular/material/dialog'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; + +import { IRuleSettingsDialogParams } from './interfaces/interfaces'; + +@Component({ + imports: [ + CommonModule, + MatButtonModule, + MatDialogModule, + MatFormFieldModule, + MatInputModule + ], + selector: 'gf-rule-settings-dialog', + standalone: true, + styleUrls: ['./rule-settings-dialog.scss'], + templateUrl: './rule-settings-dialog.html' +}) +export class GfRuleSettingsDialogComponent { + public settings: PortfolioReportRule['settings']; + + public constructor( + @Inject(MAT_DIALOG_DATA) public data: IRuleSettingsDialogParams, + public dialogRef: MatDialogRef + ) { + console.log(this.data.rule); + + this.settings = this.data.rule.settings; + } +} diff --git a/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html b/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html new file mode 100644 index 000000000..e24db29f7 --- /dev/null +++ b/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html @@ -0,0 +1,23 @@ +
{{ data.rule.name }}
+ +
+ + Threshold Min + + + + Threshold Max + + +
+ +
+ + +
diff --git a/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.scss b/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.scss new file mode 100644 index 000000000..dc9093b45 --- /dev/null +++ b/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.scss @@ -0,0 +1,2 @@ +:host { +} diff --git a/apps/client/src/app/components/rule/rule.component.html b/apps/client/src/app/components/rule/rule.component.html index 80b442b7b..f19436aba 100644 --- a/apps/client/src/app/components/rule/rule.component.html +++ b/apps/client/src/app/components/rule/rule.component.html @@ -62,6 +62,11 @@ + @if (rule?.isActive && !isEmpty(rule.settings) && false) { + + }