diff --git a/CHANGELOG.md b/CHANGELOG.md index c48547dc..7a9ac2e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added support for filtering in the _Copy AI prompt to clipboard_ actions on the analysis page (experimental) + ### Changed - Improved the symbol validation in the _Yahoo Finance_ service (get asset profiles) @@ -15,6 +19,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed an issue in the activities import functionality related to the account balances +- Changed client-side dates to be sent in UTC format to ensure date consistency + - Benchmark endpoint + - Exchange rate endpoint ## 2.146.0 - 2025-03-15 diff --git a/apps/api/src/app/endpoints/ai/ai.controller.ts b/apps/api/src/app/endpoints/ai/ai.controller.ts index 910abbf9..980d5607 100644 --- a/apps/api/src/app/endpoints/ai/ai.controller.ts +++ b/apps/api/src/app/endpoints/ai/ai.controller.ts @@ -1,5 +1,6 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; +import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { DEFAULT_CURRENCY, DEFAULT_LANGUAGE_CODE @@ -8,7 +9,14 @@ import { AiPromptResponse } from '@ghostfolio/common/interfaces'; import { permissions } from '@ghostfolio/common/permissions'; import type { AiPromptMode, RequestWithUser } from '@ghostfolio/common/types'; -import { Controller, Get, Inject, Param, UseGuards } from '@nestjs/common'; +import { + Controller, + Get, + Inject, + Param, + Query, + UseGuards +} from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; @@ -18,6 +26,7 @@ import { AiService } from './ai.service'; export class AiController { public constructor( private readonly aiService: AiService, + private readonly apiService: ApiService, @Inject(REQUEST) private readonly request: RequestWithUser ) {} @@ -25,9 +34,23 @@ export class AiController { @HasPermission(permissions.readAiPrompt) @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async getPrompt( - @Param('mode') mode: AiPromptMode + @Param('mode') mode: AiPromptMode, + @Query('accounts') filterByAccounts?: string, + @Query('assetClasses') filterByAssetClasses?: string, + @Query('dataSource') filterByDataSource?: string, + @Query('symbol') filterBySymbol?: string, + @Query('tags') filterByTags?: string ): Promise { + const filters = this.apiService.buildFiltersFromQueryParams({ + filterByAccounts, + filterByAssetClasses, + filterByDataSource, + filterBySymbol, + filterByTags + }); + const prompt = await this.aiService.getPrompt({ + filters, mode, impersonationId: undefined, languageCode: diff --git a/apps/api/src/app/endpoints/ai/ai.module.ts b/apps/api/src/app/endpoints/ai/ai.module.ts index 5a30f326..584f2995 100644 --- a/apps/api/src/app/endpoints/ai/ai.module.ts +++ b/apps/api/src/app/endpoints/ai/ai.module.ts @@ -7,6 +7,7 @@ import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.servic import { RulesService } from '@ghostfolio/api/app/portfolio/rules.service'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { UserModule } from '@ghostfolio/api/app/user/user.module'; +import { ApiModule } from '@ghostfolio/api/services/api/api.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; @@ -25,6 +26,7 @@ import { AiService } from './ai.service'; @Module({ controllers: [AiController], imports: [ + ApiModule, ConfigurationModule, DataProviderModule, ExchangeRateDataModule, diff --git a/apps/api/src/app/endpoints/ai/ai.service.ts b/apps/api/src/app/endpoints/ai/ai.service.ts index d9090d77..8807e67b 100644 --- a/apps/api/src/app/endpoints/ai/ai.service.ts +++ b/apps/api/src/app/endpoints/ai/ai.service.ts @@ -1,4 +1,5 @@ import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; +import { Filter } from '@ghostfolio/common/interfaces'; import type { AiPromptMode } from '@ghostfolio/common/types'; import { Injectable } from '@nestjs/common'; @@ -8,12 +9,14 @@ export class AiService { public constructor(private readonly portfolioService: PortfolioService) {} public async getPrompt({ + filters, impersonationId, languageCode, mode, userCurrency, userId }: { + filters?: Filter[]; impersonationId: string; languageCode: string; mode: AiPromptMode; @@ -21,6 +24,7 @@ export class AiService { userId: string; }) { const { holdings } = await this.portfolioService.getDetails({ + filters, impersonationId, userId }); diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts index 612da1ef..495a94d8 100644 --- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts +++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts @@ -190,7 +190,10 @@ export class AnalysisPageComponent implements OnDestroy, OnInit { } this.dataService - .fetchPrompt(mode) + .fetchPrompt({ + mode, + filters: this.userService.getFilters() + }) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(({ prompt }) => { this.clipboard.copy(prompt); diff --git a/apps/client/src/app/services/admin.service.ts b/apps/client/src/app/services/admin.service.ts index 5d71a80f..fea3924e 100644 --- a/apps/client/src/app/services/admin.service.ts +++ b/apps/client/src/app/services/admin.service.ts @@ -8,7 +8,6 @@ import { PROPERTY_API_KEY_GHOSTFOLIO } from '@ghostfolio/common/config'; import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config'; -import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { AssetProfileIdentifier, AdminData, @@ -25,7 +24,6 @@ import { Injectable } from '@angular/core'; import { SortDirection } from '@angular/material/sort'; import { DataSource, MarketData, Platform } from '@prisma/client'; import { JobStatus } from 'bull'; -import { format } from 'date-fns'; import { switchMap } from 'rxjs'; import { environment } from '../../environments/environment'; @@ -186,19 +184,8 @@ export class AdminService { ); } - public gatherSymbol({ - dataSource, - date, - symbol - }: AssetProfileIdentifier & { - date?: Date; - }) { - let url = `/api/v1/admin/gather/${dataSource}/${symbol}`; - - if (date) { - url = `${url}/${format(date, DATE_FORMAT)}`; - } - + public gatherSymbol({ dataSource, symbol }: AssetProfileIdentifier) { + const url = `/api/v1/admin/gather/${dataSource}/${symbol}`; return this.http.post(url, {}); } diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index e8d4b782..4eba3fff 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -57,6 +57,7 @@ import { translate } from '@ghostfolio/ui/i18n'; import { HttpClient, HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { SortDirection } from '@angular/material/sort'; +import { utc } from '@date-fns/utc'; import { AccountBalance, DataSource, @@ -281,7 +282,7 @@ export class DataService { symbol: string; }) { return this.http.get( - `/api/v1/exchange-rate/${symbol}/${format(date, DATE_FORMAT)}` + `/api/v1/exchange-rate/${symbol}/${format(date, DATE_FORMAT, { in: utc })}` ); } @@ -363,10 +364,7 @@ export class DataService { } return this.http.get( - `/api/v1/benchmarks/${dataSource}/${symbol}/${format( - startDate, - DATE_FORMAT - )}`, + `/api/v1/benchmarks/${dataSource}/${symbol}/${format(startDate, DATE_FORMAT, { in: utc })}`, { params } ); } @@ -655,8 +653,18 @@ export class DataService { return this.http.get('/api/v1/portfolio/report'); } - public fetchPrompt(mode: AiPromptMode) { - return this.http.get(`/api/v1/ai/prompt/${mode}`); + public fetchPrompt({ + filters, + mode + }: { + filters?: Filter[]; + mode: AiPromptMode; + }) { + const params = this.buildFiltersAsQueryParams({ filters }); + + return this.http.get(`/api/v1/ai/prompt/${mode}`, { + params + }); } public fetchPublicPortfolio(aAccessId: string) { diff --git a/package-lock.json b/package-lock.json index a6e4d4dd..407107fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@angular/router": "19.2.1", "@angular/service-worker": "19.2.1", "@codewithdan/observable-store": "2.2.15", + "@date-fns/utc": "2.1.0", "@dfinity/agent": "0.15.7", "@dfinity/auth-client": "0.15.7", "@dfinity/candid": "0.15.7", @@ -3113,6 +3114,11 @@ "dev": true, "license": "MIT" }, + "node_modules/@date-fns/utc": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@date-fns/utc/-/utc-2.1.0.tgz", + "integrity": "sha512-176grgAgU2U303rD2/vcOmNg0kGPbhzckuH1TEP2al7n0AQipZIy9P15usd2TKQCG1g+E1jX/ZVQSzs4sUDwgA==" + }, "node_modules/@dfinity/agent": { "version": "0.15.7", "resolved": "https://registry.npmjs.org/@dfinity/agent/-/agent-0.15.7.tgz", diff --git a/package.json b/package.json index eb3a7083..89ca173a 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "@angular/router": "19.2.1", "@angular/service-worker": "19.2.1", "@codewithdan/observable-store": "2.2.15", + "@date-fns/utc": "2.1.0", "@dfinity/agent": "0.15.7", "@dfinity/auth-client": "0.15.7", "@dfinity/candid": "0.15.7",